> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pipefort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CICD-SEC-8 — Sensitive job on a self-hosted runner with no declared egress restriction

> A runner on your network, holding the job's secrets, able to reach anywhere.

| Field      | Value                                                                                                                                                              |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Rule ID    | `cicd-sec-8-selfhosted-egress`                                                                                                                                     |
| Category   | `CICD-SEC-8`                                                                                                                                                       |
| Severity   | **MEDIUM**                                                                                                                                                         |
| Confidence | MEDIUM                                                                                                                                                             |
| Platforms  | GitHub Actions **and** GitLab CI (one rule ID)                                                                                                                     |
| OWASP      | [CICD-SEC-8: Ungoverned Usage of 3rd Party Services](https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-08-Ungoverned-Usage-of-3rd-Party-Services) |
| Auto-fix   | ✗ (advisory)                                                                                                                                                       |

## What the check does

Flags a job that runs on a **self-hosted runner** *and* does at least one
sensitive thing, with **no egress policy declared**.

## Why it matters

A GitHub-hosted runner is a fresh VM destroyed after the job. A self-hosted
runner is a machine on your network, and by default it can reach the whole
internet with the job's secrets in scope.

That is the shape exfiltration takes when it does not need an exploit: the job
legitimately holds a deploy key and legitimately makes network calls, and
nothing distinguishes the call that ships the build from the call that ships the
key.

## Why this is not `best-prac-3-self-hosted-runners`

[That rule](/rules/best-prac-3) reports the *fact* of a self-hosted runner,
tiered to the `auditor` persona because it is usually a deliberate infra choice.
This rule fires only where the combination bites — self-hosted **and** something
worth stealing:

| Sensitive trait                  | Detected by                                                                                   |
| -------------------------------- | --------------------------------------------------------------------------------------------- |
| Consumes repository secrets      | `secrets.` in the job `env:` or any step's `env:` / `with:` / `run:` / `if:`                  |
| Runs under `pull_request_target` | Untrusted input, in a privileged context, **on your own hardware**                            |
| Publishes a release or package   | A publish action, or a publish command (`npm publish`, `docker push`, `gh release create`, …) |

A self-hosted job that only runs `make lint` stays quiet. So does a
GitHub-hosted job holding secrets.

## Two ways to satisfy it

### 1. Declare a policy in the workflow

`step-security/harden-runner` is the one egress policy declarable inside a
workflow file, so the rule reads it:

```yaml theme={null}
- uses: step-security/harden-runner@v2
  with:
    egress-policy: block          # ← satisfies the rule
    allowed-endpoints: >
      api.github.com:443
```

`egress-policy: audit` does **not** satisfy it — audit mode observes egress
without restricting it. Rather than staying silent, the finding names what it
found:

> …the job runs step-security/harden-runner with egress-policy: audit, which
> observes egress but does not restrict it.

### 2. Record that egress is governed elsewhere

Firewalls, network policies, and proxies live in runner infrastructure the
scanner cannot see. So the rule ships its own acknowledgment:

```yaml theme={null}
# pipefort: egress-restricted     ← column 0: covers every job in the file
on: push
jobs:
  # pipefort: egress-restricted   ← indented: covers just this job
  deploy:
    runs-on: self-hosted
```

**Nesting is the scope.** An unindented directive covers the file; one placed
above a job covers that job.

This is a *dedicated* directive rather than the generic
[`# pipefort: ignore[...]`](/cli/configuration) because the two mean different
things. `ignore` says "do not tell me about this". `egress-restricted` says
"this is handled" — and it reads that way to the next person in the diff.

## GitLab CI

The same rule ID. GitLab has no `self-hosted` label — a job reaches a specific
runner through `tags:`, so a tag that is not one of GitLab's `saas-*` runner
names means somebody's own machine. Sensitivity is a `CI_*_TOKEN`-family or
credential-shaped variable reference, or a publish command.

```yaml theme={null}
deploy:
  tags:
    - internal-runner        # ← not a saas-* runner
  script:
    - ./deploy.sh "$CI_DEPLOY_TOKEN"
```

## Auto-fix

**None** — advisory. Egress control lives in runner infrastructure, not in the
workflow file, so there is nothing for a fixer to write. The two surfaces above
are how you resolve the finding.

## Related rules

* [BEST-PRAC-3 — Self-hosted runner usage](/rules/best-prac-3)
* [CICD-SEC-8 — `repository_dispatch` without a `types:` allowlist](/rules/cicd-sec-8)
* [CICD-SEC-6 — Run step dumps the environment or echoes a CI token](/rules/cicd-sec-6-env-exfil)
* [CICD-SEC-1 — `pull_request_target` trigger boundary confusion](/rules/cicd-sec-1-pr-target-dual-trigger)
