Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ jobs:
no-cache: true
load: true
tags: buildcage-test
# Only Dockerfile.transparent-restrict mounts this; an unmounted secret is never materialized.
secrets: |
testtoken=buildcage-secret-mount-probe

- name: Show logs
if: always()
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ TEST_COMPOSE_FILE ?= compose.test-transparent.yaml
setup_buildkit_% test_integration_buildkit_% example_% clean_buildkit report_buildkit: export COMPOSE_PROJECT_NAME := buildcage-project
setup_buildkit_% test_integration_buildkit_% example_% clean_buildkit report_buildkit: export BUILDCAGE_BUILD_TEST_HOOKS := 1

# Dummy value for the --secret mount exercised by Dockerfile.transparent-restrict.
# Only its presence/absence per RUN is asserted, never its contents.
test_integration_buildkit_%: export BUILDCAGE_TEST_SECRET := buildcage-secret-mount-probe

.PHONY: help
help:
@grep -E '^[a-zA-Z_0-9-]+(-%)?:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down Expand Up @@ -154,6 +158,7 @@ test_integration_buildkit_transparent_restrict: ## Run transparent-engine restri
@docker buildx build --no-cache \
--builder buildcage \
--platform linux/arm64 \
--secret id=testtoken,env=BUILDCAGE_TEST_SECRET \
--progress=plain -f test/Dockerfile.transparent-restrict test/ \
--load -t buildcage-test
@node report/src/main.ts || true
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,18 @@ delivered through an allowed domain still runs. Use it as one layer in a defense
a last line of defense so that if something slips through your other measures, at least it can't
call home. See [Security Details](./docs/security.md) for the full threat model.

That boundary is also why the allowlist works best alongside limiting which secrets the build can
see. A token the build doesn't need shouldn't be in it at all, so publish from a step outside the
build. A token it does need can be scoped to the single `RUN` that needs it:

```dockerfile
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
RUN npm run build
```

Both `RUN` steps can still reach `registry.npmjs.org`, but only the first one holds a credential to
use against it. See [Limiting what the build can see](./docs/security.md#limiting-what-the-build-can-see).

## Documentation

| Doc | What's in it |
Expand Down
34 changes: 34 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,40 @@ Given these implementation costs versus the strict preconditions for the attack
- **Major CDN countermeasures** — Major CDN providers like CloudFront and Cloudflare have already introduced measures to restrict domain fronting. Consult your CDN provider's documentation for current details.
- **Regular audits** — Periodically run in [audit mode](../README.md#operation-modes) to detect anomalies in connection patterns.

## Limiting What the Build Can See

An allowlist decides which destinations a build can reach. It cannot tell a legitimate use of an
allowed destination from an abusive one.

Concretely: a compromised dependency that runs during your build can read `~/.npmrc`, take the npm
token, and publish a malicious version to `registry.npmjs.org`. That destination is on your
allowlist, and has to be for `npm ci` to work at all, so nothing in the rule set stops it. The same
shape applies to any service the build both authenticates to and is allowed to reach.

The answer is not a stricter allowlist. It's to keep the credential away from the code that might
abuse it.

### Don't put a token in the build unless the build needs it

Publishing, releasing, and deploying do not have to happen inside `docker build`. A token used by a
later workflow step is never exposed to dependency code running in a `RUN` step. Where a token is
unavoidable, prefer one scoped to reading.

### Scope the tokens it does need to a single RUN

Buildcage does not change how BuildKit handles secrets, so
[`RUN --mount=type=secret`](https://docs.docker.com/build/building/secrets/) works as usual through
the remote driver. The secret is materialized only for the `RUN` that mounts it:

```dockerfile
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
RUN npm run build
```

Both steps can still reach `registry.npmjs.org`, but only the first holds a credential to use
against it. Now that npm makes install scripts opt-in, dependency code usually first executes during
`npm run build`, where there is nothing left to publish with.

## Explicit Proxy Engine

> [!WARNING]
Expand Down
14 changes: 14 additions & 0 deletions test/Dockerfile.transparent-restrict
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ RUN echo "=== [npm install - allowed registry] ===" && \
(npm ci --ignore-scripts >/dev/null 2>&1 && \
echo "✓ npm install express: OK") || (echo "✗ npm install express: FAILED" && exit 1)

# [secret mount - readable by the RUN that mounts it]
# Backs the hardening guidance in docs/security.md: BuildKit's own secret
# mounts still work through the remote driver, so a token can be scoped to the
# one RUN that needs it rather than left where dependency code later runs.
RUN --mount=type=secret,id=testtoken \
echo "=== [secret mount - mounted RUN] ===" && \
(test -s /run/secrets/testtoken && echo "✓ secret readable in the RUN that mounts it") || \
(echo "✗ secret not readable in the RUN that mounts it" && exit 1)

# [secret mount - absent from a RUN that does not mount it]
RUN echo "=== [secret mount - later RUN] ===" && \
(test ! -e /run/secrets/testtoken && echo "✓ secret absent from a RUN that does not mount it") || \
(echo "✗ secret leaked into a RUN that does not mount it" && exit 1)

# Network configuration check
RUN echo "=== Network Configuration ===" && \
ip route show && \
Expand Down