Back

CICD-SEC-8: Ungoverned Usage of 3rd Party Services - Examples

Each pair below shows an ungoverned use of a third-party service and the governed version of the same thing. The examples focus on the choices that dominate real findings: unpinned third-party Actions, over-scoped Apps and OAuth grants, long-lived tokens handed to SaaS tools, and unrestricted marketplace usage.

1. Third-Party GitHub Action Reference

Ungoverned

# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: some-user/awesome-action@v2      # mutable tag, unknown author
      - uses: another/deploy-step@main         # HEAD of a repo you don't control
    # If @v2 or @main is repointed to malicious code, your next build runs it
    # with the job's secrets and workspace in scope.

Governed

# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      # Third-party step pinned to a full commit SHA (immutable), version in comment
      - uses: some-user/awesome-action@3f1c0a9d6b2e4f8a1c7d5e2b9f0a4c6d8e1b3a5c  # v2.3.1
      # Prefer first-party / vendored copies for anything security-critical
      - uses: our-org/deploy-step@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0    # internal fork
    # Pins are updated only via a reviewed PR from a dependency-update bot.

2. SCM App / OAuth Scope

Ungoverned

# App authorized "to be safe" - one click grants everything
permissions:
  repositories: all              # every repo in the org
  contents: read-write           # a read-only scanner asking for write
  members: read                  # org-wide membership visibility
  administration: read           # far more than the feature needs
  webhooks: read-write           # can add its own persistence
# Granted once in 2024, never reviewed since.

Governed

# App scoped to exactly what the stated feature requires
permissions:
  repositories: [web-app]        # only the repo it serves
  contents: read                 # read-only scanner => read only
  metadata: read
  pull_requests: read            # needed to comment on PRs, nothing more
owner: dev-experience-team
review_by: 2026-07-10            # access expires unless re-justified
# Installation restricted to org admins; approval required to add.

3. SaaS Tool Credential in CI

Ungoverned

# Long-lived, broad personal access token stored with a third-party SaaS
env:
  COVERAGE_TOKEN: ghp_************************   # classic PAT, "repo" scope, no expiry
steps:
  - run: bash <(curl -s https://cdn.thirdparty.example/uploader.sh)
    # The vendor now stores YOUR broad token; the script is fetched fresh each run.
    # Vendor breach OR script tampering => your secrets and source are exposed.

Governed

# No stored static token: exchange a per-run OIDC identity for short-lived access
permissions:
  id-token: write                # allow OIDC token minting for this job only
  contents: read
steps:
  # Pin the uploader to a SHA instead of piping a live-fetched script to bash
  - uses: vendor/coverage-uploader@9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6  # v4.2.0
    with:
      use-oidc: true             # temporary, minimally-scoped credential
    # Credential is minted per run, expires in minutes, and is scoped to upload only.

4. Restricting Which Actions/Apps May Run (Org Policy)

Ungoverned

# Org settings
allowed_actions: all             # any Action published anywhere may run
marketplace_apps: "any member may install any app"
# Any developer can pull in any third-party component or authorize any App.

Governed

# Org settings
allowed_actions: selected
allow:
  - "actions/*@*"                # first-party
  - "our-org/*@*"                # internal, trusted
  - "verified-vendor/tool@9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6"  # reviewed + pinned
deny_by_default: true            # anything not listed cannot run
marketplace_apps:
  installation: "org-admin approval required"
  third_party_by_default: denied

5. Webhook Handling

Ungoverned

# Inbound webhook triggers a deploy with no verification
@app.route('/webhooks/deploy', methods=['POST'])
def deploy():
    trigger_deploy(request.json['ref'])     # anyone who knows the URL can deploy
    return '', 200
# No signature check => forged or replayed events trigger real deploys.

Governed

import hmac, hashlib
from flask import request, abort

WEBHOOK_SECRET = load_secret('deploy_webhook')   # not hard-coded

@app.route('/webhooks/deploy', methods=['POST'])
def deploy():
    sig = request.headers.get('X-Hub-Signature-256', '')
    expected = 'sha256=' + hmac.new(WEBHOOK_SECRET, request.data, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):   # constant-time verification
        abort(401)
    trigger_deploy(request.json['ref'])
    return '', 200
# Unsigned or tampered events are rejected before any deploy happens.

6. Access Lifecycle

Ungoverned

Integration added -> works forever.
  - No inventory of who-has-access.
  - No owner, no review date.
  - Deploy keys and bot tokens from finished projects remain, still write-capable.
  - A departed contractor's OAuth app is still authorized org-wide.

Governed

# Every grant is inventoried, owned, and expires unless re-justified
- name: legacy-migration-key
  type: deploy-key
  owner: platform-team
  added: 2025-02-01
  review_by: 2025-08-01          # overdue => auto-flagged for revocation
  status: REVOKE (project complete)
# Offboarding a person/project revokes their grants, tokens, and bots.
# Quarterly review reconciles inventory against the platform's actual grants.

What Changed, and Why

ConcernUngovernedGoverned
Component referenceMutable tag/branch from unknown authorFull commit SHA, verified/first-party preferred
App / OAuth scopeOrg-wide, read/write, "to be safe"Specific repos, read-only where possible, owned
SaaS credentialLong-lived broad PAT stored with vendorShort-lived OIDC, minted per run, upload-only
Marketplace usageAnything may run; anyone may installOrg allow-list + admin approval, deny by default
WebhooksUnverified triggers, secrets in payloadsSignature-verified, minimal data out
LifecycleGranted once, never reviewedInventoried, review-dated, revoked on offboard

Next Steps