Runseal was built to solve the problem of software supply chain attacks that are often triggered from GitHub Actions-based exploits.
By using [nono's](https://github.com/always-further/nono) strong kernel-enforced sandboxing, runseal can protect secrets/tokens, sensitive files, and network access from untrusted or malicious code, while still allowing necessary software engineering operations through a flexible policy system.
The project is developed by the engineers behind [sigstore](https://sigstore.dev) and [nono](https://nono.sh).
## What Runseal Does
- Replace raw secrets within a workflow with phantom credentials that are useless if leaked
- Protects sensitive files and secrets from exfiltration by untrusted code in CI
- L7 network filtering to lock down network access by HTTP method and path
- Uses `nono` TLS interception so HTTPS requests can be filtered by method/path
- Blocks network by default unless policy explicitly allows a host or credential route
- Restricts filesystem reads and writes to declared paths
- Cryptographic audit captured outside of the sandbox for all network requests, credential injections, and filesystem access
## Quick Start
name: Publish
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: always-further/runseal@v1
with:
run: npm publish
policy: |
fs:
read: ["."]
write: []
network:
mode: filtered
access:
npm:
secret: NPM_TOKEN
url: https://registry.npmjs.org
allow:
- PUT /**
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
In this example, `npm publish` can read the repository, cannot write to paths
not listed in `fs.write`, cannot use general network access, and can only use
`NPM_TOKEN` through the Runseal/nono proxy for allowed HTTPS requests to
`registry.npmjs.org`.
## Policy Format
Runseal policy is YAML passed through the `policy` input.
fs:
read:
- "."
- "$HOME/.cache/my-tool"
write:
- "./dist"
network:
mode: filtered
allow:
- api.github.com
access:
deploy:
secret: DEPLOY_TOKEN
url: https://api.example.com
allow:
- POST /v1/deployments
- GET /v1/deployments/*
### Filesystem Access
`fs.read` lists paths the command can read. `fs.write` lists paths the command
can write.
Keep these narrow. For example, a deploy step often only needs to read `./dist`
and a config file, and may not need write access at all.
fs:
read: ["./dist", "./fly.toml"]
write: []
### Network Access
Runseal expects `network.mode: blocked` or `network.mode: filtered`.
Add `network.allow` only for unauthenticated hosts the command must reach. Hosts
used by access grants are added to the generated `nono` profile automatically.
network:
mode: filtered
allow:
- api.github.com
### Access Grants
Each key under `access` is a named grant. `secret` is the environment variable
containing the real secret, `url` is the service base URL, and `allow` lists the
HTTP routes where the secret may be injected. Runseal masks the secret in logs,
writes it to a private file, removes it from the child environment, and
configures `nono` to inject it through the local proxy.
access:
fly:
secret: FLY_API_TOKEN
url: https://api.machines.dev
allow:
- POST /v1/apps/*/machines
The sandboxed command receives a phantom credential for SDK compatibility. The
real secret remains outside the sandbox and is only inserted by the proxy when
the host and endpoint policy match.
### HTTPS Endpoint Filtering
`allow` restricts access use by HTTP method and path. Matching is allow-list
based.
allow:
- POST /v1/apps/*/releases
- GET /v1/apps/*/status
Runseal relies on `nono` TLS interception for this. The `nono` proxy creates an
ephemeral trust bundle and injects standard CA environment variables into the
sandboxed process, so common HTTPS clients can connect through the proxy while
still allowing L7 policy enforcement.
## Common Recipes
### Run Tests With No Network
- uses: always-further/runseal@v1
with:
run: npm test
policy: |
fs:
read: [".", "./node_modules"]
write: ["./coverage"]
network:
mode: blocked
### Build With Package Registry Access
- uses: always-further/runseal@v1
with:
run: npm ci
policy: |
fs:
read: ["."]
write: ["./node_modules"]
network:
mode: filtered
allow:
- registry.npmjs.org
### Deploy With A Sealed Token
- uses: always-further/runseal@v1
with:
run: ./scripts/deploy.sh
policy: |
fs:
read: ["./dist", "./deploy.yaml"]
write: []
network:
mode: filtered
access:
deploy:
secret: DEPLOY_TOKEN
url: https://deploy.example.com
allow:
- POST /v1/releases
- GET /v1/releases/*
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
## Inputs
| Input | Required | Default | Description |
| --- | --- | --- | --- |
| `run` | Yes | none | Command to execute inside the sandbox. |
| `policy` | No | empty | Runseal policy YAML. Prefer this for new workflows. |
| `fs-read` | No | empty | Comma-separated read paths when `policy` is not set. |
| `fs-write` | No | empty | Comma-separated write paths when `policy` is not set. |
| `network` | No | `blocked` | Network policy when `policy` is not set: `blocked` or comma-separated domains. |
| `runseal-version` | No | `latest` | Runseal release version to install. Accepts `v0.1.0` or `0.1.0`. |
| `nono-version` | No | `latest` | nono release version to install. Accepts `v0.1.0` or `0.1.0`. |
| `verify-attestations` | No | `true` | Verify GitHub artifact attestations for downloaded release assets. |
| `audit` | No | `false` | Set to `artifact` or `true` to upload nono audit evidence as a GitHub Actions artifact. |
## Audit Evidence
Runseal can export the nono audit session for a sandboxed command:
- uses: always-further/runseal@v1
with:
run: npm rebuild
audit: artifact
policy: |
fs:
read: [".", "./node_modules"]
write: ["./node_modules"]
network:
mode: blocked
When enabled, Runseal captures the new nono audit session after the command
finishes and uploads a `runseal-audit` artifact containing:
- `summary.md`
- one JSON file per detected nono audit session
Audit export runs before Runseal returns the sandboxed command's exit status, so
failed or denied commands can still produce audit evidence.
## Supply Chain Verification
- SHA-256 checksum from the release `SHA256SUMS` file
- GitHub artifact attestation proving the asset was built by the expected
repository and tag, for both `always-further/runseal` and `always-further/nono`
Attestation verification uses `gh attestation verify` and is enabled by default.
Set `verify-attestations: false` only for local testing or emergency fallback.
## Requirements
- Linux x86_64 GitHub-hosted runner
- `gh` CLI available on the runner for attestation verification
- Published release assets for both Runseal and `nono`
Release assets are expected to use this naming scheme:
- `runseal-v
-x86_64-unknown-linux-gnu.tar.gz`
- `nono-v-x86_64-unknown-linux-gnu.tar.gz`
- `SHA256SUMS`
## Development
make ci
`make ci` runs `make lint` and `make test` — the same Rust checks as the [CI workflow](.github/workflows/ci.yml).
make lint # clippy + fmt check
make test # unit tests only
make fmt # format code
make audit # cargo audit (run make audit-install first)