# From audit to enforce

> Build a job's allowlist from audit runs, check it on pull requests and enforce it, without breaking the pipeline.

A new policy starts in `audit`: nothing is refused and the job summary shows what `enforce` would refuse. Once the summary is clean for the jobs you care about, `enforce` refuses the rest.

## 1. Run in audit

```yaml
version: 1
mode: audit
presets: [github-actions]
allow: []
```

Run every workflow that uses the action at least once, including the jobs that only run on `main`, on tags or on a schedule: a job's destinations depend on what it does.

## 2. Copy the names

Each job summary has a collapsed block **Names outside the policy**, already in YAML:

```yaml
allow:
  - proxy.golang.org
  - sum.golang.org
  - registry.npmjs.org
```

Add them to `allow`, after checking that each one is expected. A name you do not recognise is the point of the exercise: find which step reached it before allowing it. The **Via** column helps: `direct` means a tool ignored `HTTPS_PROXY`, `container` names the container.

Replace many names under one domain by a wildcard only when the names vary between runs (CDN shards, per-region hosts). `*.example.com` allows every name below `example.com`, which is more than the job needs.

## 3. Check on pull requests

The `mode` input overrides the file, so one policy can be enforced on pull requests while `main` still only reports:

```yaml
- uses: openwaf-dev/egress-action@v0.3.2
  with:
    mode: ${{ github.event_name == 'pull_request' && 'enforce' || 'audit' }}
```

A refused connection fails the step that made it (the tool gets `403`, `NXDOMAIN` or a reset), and the summary says which name and why.

## 4. Enforce

Set `mode: enforce` in the file. For a job on a GitHub-hosted runner, also ask for the lock and for every container to be held to the policy:

```yaml
- uses: openwaf-dev/egress-action@v0.3.2
  with:
    lock: "on"
    containers: lock-all
```

With `lock: "on"` the step fails when root is not reachable, instead of running with only the proxy; in enforce, a lock that fails to install fails the step too. `lock-all` is safe on GitHub-hosted runners, which run one job per machine.

## Keeping the policy current

- **New dependency or tool:** the first run in enforce fails on the new name; the summary gives the `allow` line.
- **A name that only appears sometimes:** run the job in audit (the `mode` input) a few times and compare summaries.
- **Switch off quickly:** set `mode: audit` in the input or the file. To turn it off without a commit, read the mode from a repository variable: `mode: ${{ vars.EGRESS_MODE || 'enforce' }}`, then `gh variable set EGRESS_MODE -b audit`.

## Troubleshooting

| Symptom | Cause and fix |
|---|---|
| `policy .github/egress.yml not found` | The action runs before `actions/checkout`, or the file has another path: use the `policy` input. |
| `field … not found in type main.Policy` | A typo in a key of `egress.yml`. |
| `enforce needs the lock` | Root was reachable but the nftables lock could not be installed, usually because `nft` is missing on a self-hosted runner. See [self-hosted runners](https://developers.openwaf.dev/egress/ci/self-hosted-runners/#requirements). |
| `lock: on needs sudo or Docker` | `lock: "on"` on a runner where the job user has neither passwordless `sudo` nor Docker. |
| A tool fails with a certificate error | The host is under `inspect` and the tool does not read the CA variables. See [Inspection and DLP](https://developers.openwaf.dev/egress/ci/inspect-and-dlp/#the-tls-trade-off). |
| The cache step is slow or refused | It needs the `github-actions` preset: the Actions cache uses GitHub's storage accounts. |
| An allowed name is refused with `name_ip_mismatch` | The connection went to an address that name does not resolve to: an entry in `/etc/hosts`, a hard-coded address or a tool's own DNS over HTTPS. Let the tool resolve the name through the system resolver. |
