Skip to content

Commit 48b0940

Browse files
committed
feat: sealed cargo-docker + publish-dry-run; harden the Docker pattern
Centralize the Docker actions' docker run in a shared _lib/seal.sh: non-root, --cap-drop=ALL, --security-opt=no-new-privileges, repo mounted read-only, and --network=none when sealed. The containment policy now lives in one place. Add a low-level cargo-docker (run any cargo command sealed) and a publish-dry-run playbook that splits the release gate into network-with-no-code then code-with-no-network: a networked prep runs the publish checks WITHOUT a build (cargo fetch + tag/version + crates.io probe + cargo publish --dry-run --no-verify), then the verify-BUILD runs SEALED (cargo package --offline, --network=none) -- so dependency build.rs / proc-macros cannot reach the network during the release gate. Refactor lint-and-test-docker + cargo-fetch onto seal.sh (behavior preserved, now hardened). Retire check-release-readiness-docker (superseded; no consumers). Extend selftest with a sealed cargo-docker build, the publish-dry-run, and a negative control proving --network=none blocks egress.
1 parent 0ec0838 commit 48b0940

12 files changed

Lines changed: 449 additions & 244 deletions

File tree

‎.github/actions/_lib/seal.sh‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
# Shared sealed-runner for the Docker actions. Source it, then call `seal_run`:
3+
#
4+
# source "$GITHUB_ACTION_PATH/../_lib/seal.sh"
5+
# seal_run bash /cicd/<script>.sh # or: seal_run bash -c '<inline>'
6+
#
7+
# It owns the single hardened `docker run` so the containment policy lives in one
8+
# place: non-root (`--user`), all Linux capabilities dropped (`--cap-drop=ALL`),
9+
# no privilege escalation (`--security-opt=no-new-privileges`), the repo mounted
10+
# read-only, and — when OFFLINE=true — sealed with `--network=none` so a build
11+
# (and the dependency build.rs / proc-macros it runs) cannot reach the network.
12+
# Run cargo-fetch first to warm the cache for the sealed (offline) path.
13+
#
14+
# Config is read from the environment (an action sets these before calling):
15+
# IMAGE (required) the CI image to run
16+
# CARGO_CACHE (required) host dir for CARGO_HOME (registry/git cache), mounted RW
17+
# OFFLINE "true" → --network=none (sealed); anything else → networked
18+
# TARGET_DIR if non-empty: mount it RW at /work/target + set CARGO_TARGET_DIR
19+
# CICD_DIR if non-empty: mount it RO at /cicd (so `bash /cicd/<script>` resolves)
20+
# ENV_INCLUDE / ENV_EXCLUDE POSIX-ERE name filters for env forwarding (exclusion wins)
21+
# INPUT_ENV extra literal KEY=VALUE lines (the action's `env` input), forwarded verbatim
22+
# EXTRA_ENV extra literal KEY=VALUE lines the action itself injects (e.g. ARGS, a masked token)
23+
# Args ("$@"): the command to exec inside the container.
24+
#
25+
# The owner vars (CARGO_HOME / RUSTUP_HOME / CARGO_TARGET_DIR) are pinned as explicit
26+
# `-e` AFTER --env-file (last-wins), so no env-include/-exclude tinkering can redirect
27+
# the mounted cache or target.
28+
29+
seal_run() {
30+
: "${IMAGE:?seal_run: IMAGE required}" "${CARGO_CACHE:?seal_run: CARGO_CACHE required}"
31+
mkdir -p "$CARGO_CACHE"
32+
33+
local net=""
34+
[ "${OFFLINE:-}" = "true" ] && net="--network=none"
35+
36+
local target_args=()
37+
if [ -n "${TARGET_DIR:-}" ]; then
38+
mkdir -p "$TARGET_DIR"
39+
target_args=(-e CARGO_TARGET_DIR=/work/target -v "$PWD/$TARGET_DIR:/work/target")
40+
fi
41+
42+
local cicd_args=()
43+
[ -n "${CICD_DIR:-}" ] && cicd_args=(-v "$CICD_DIR:/cicd:ro")
44+
45+
# Forward env: keep names matching ENV_INCLUDE (default CARGO_.*), drop ENV_EXCLUDE
46+
# (exclusion wins), then append the literal `env` input and any action-injected EXTRA_ENV.
47+
local ef
48+
ef="$(mktemp)"
49+
env | awk -v inc="${ENV_INCLUDE:-}" -v exc="${ENV_EXCLUDE:-}" '
50+
{ name = $0; sub(/=.*/, "", name) }
51+
inc != "" && name !~ "^(" inc ")$" { next }
52+
exc != "" && name ~ "^(" exc ")$" { next }
53+
{ print }
54+
' > "$ef"
55+
[ -n "${INPUT_ENV:-}" ] && printf '%s\n' "$INPUT_ENV" >> "$ef"
56+
[ -n "${EXTRA_ENV:-}" ] && printf '%s\n' "$EXTRA_ENV" >> "$ef"
57+
58+
local rc=0
59+
# shellcheck disable=SC2086 # $net is intentionally word-split (flag or empty)
60+
docker run --rm $net \
61+
--security-opt=no-new-privileges --cap-drop=ALL \
62+
--env-file "$ef" \
63+
--user "$(id -u):$(id -g)" \
64+
-e HOME=/tmp \
65+
-e CARGO_HOME=/cache/cargo \
66+
-e RUSTUP_HOME=/usr/local/rustup \
67+
"${target_args[@]}" \
68+
-v "$PWD:/work:ro" \
69+
-v "$PWD/$CARGO_CACHE:/cache/cargo" \
70+
"${cicd_args[@]}" \
71+
-w /work \
72+
"$IMAGE" "$@" || rc=$?
73+
rm -f "$ef"
74+
return "$rc"
75+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Cargo (Docker, sealed)
2+
description: >-
3+
Run one cargo command inside the pinned container — non-root, all caps dropped,
4+
no-new-privileges, repo mounted read-only. Sealed by default (--network none +
5+
cargo --offline; run cargo-fetch first to warm the cache); set offline:false to
6+
run networked and fetch as it builds. The low-level primitive behind the sealed
7+
build / publish-dry-run steps.
8+
9+
inputs:
10+
image:
11+
description: CI image (Rust toolchain + clippy/rustfmt + jq).
12+
required: false
13+
default: ghcr.io/gronke/rust-ci:latest
14+
working-directory:
15+
description: Crate/workspace directory (mounted read-only).
16+
required: false
17+
default: "."
18+
args:
19+
description: >-
20+
The cargo command + flags, e.g. "build --release --locked --features full".
21+
Do NOT include --offline — the `offline` input controls it.
22+
required: true
23+
target-dir:
24+
description: Host dir for cargo target (read-write, cacheable).
25+
required: false
26+
default: target
27+
cargo-cache:
28+
description: Host dir for CARGO_HOME (read-write; populated by cargo-fetch).
29+
required: false
30+
default: .cargo-cache
31+
offline:
32+
description: >-
33+
Run sealed: --network none + cargo --offline (needs a prior cargo-fetch to
34+
populate the cache). Set "false" to run networked and fetch as it builds.
35+
Default "true".
36+
required: false
37+
default: "true"
38+
env-include:
39+
description: >-
40+
Regex (POSIX ERE) of env-var NAMES to forward from the runner into the
41+
container, anchored full-name. Default forwards cargo's own vars.
42+
required: false
43+
default: "CARGO_.*"
44+
env-exclude:
45+
description: >-
46+
Regex (POSIX ERE) of names to DROP from the included set (exclusion wins).
47+
Defaults to the action-owned vars; widening env-include to ".*" forwards
48+
secrets like GITHUB_TOKEN, so prefer a tight include or add to this list.
49+
required: false
50+
default: "CARGO_HOME|RUSTUP_HOME|CARGO_TARGET_DIR"
51+
env:
52+
description: >-
53+
Extra literal KEY=VALUE lines (one per line) forwarded verbatim,
54+
regardless of env-include/env-exclude.
55+
required: false
56+
default: ""
57+
58+
runs:
59+
using: composite
60+
steps:
61+
- name: cargo (sealed)
62+
shell: bash
63+
working-directory: ${{ inputs.working-directory }}
64+
env:
65+
IMAGE: ${{ inputs.image }}
66+
TARGET_DIR: ${{ inputs.target-dir }}
67+
CARGO_CACHE: ${{ inputs.cargo-cache }}
68+
OFFLINE: ${{ inputs.offline }}
69+
ENV_INCLUDE: ${{ inputs.env-include }}
70+
ENV_EXCLUDE: ${{ inputs.env-exclude }}
71+
INPUT_ENV: ${{ inputs.env }}
72+
ARGS: ${{ inputs.args }}
73+
run: |
74+
set -euo pipefail
75+
# shellcheck source=/dev/null
76+
source "$GITHUB_ACTION_PATH/../_lib/seal.sh"
77+
export CICD_DIR="$GITHUB_ACTION_PATH"
78+
# ARGS + OFFLINE aren't CARGO_* (so env-include won't forward them); inject
79+
# them as explicit env-file lines for the in-container script.
80+
EXTRA_ENV="$(printf '%s\n%s' "ARGS=$ARGS" "OFFLINE=$OFFLINE")"
81+
export EXTRA_ENV
82+
seal_run bash /cicd/cargo-docker.sh
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
# Run one cargo command inside the sealed container. ARGS is the cargo command +
3+
# flags (e.g. "build --release --locked --features full"); when OFFLINE=true,
4+
# --offline is appended (a cargo-fetch'd cache + a committed Cargo.lock are
5+
# assumed, and the action also runs --network none). Do NOT put --offline in ARGS.
6+
set -euo pipefail
7+
8+
offline_arg=""
9+
[ "${OFFLINE:-true}" = "true" ] && offline_arg="--offline"
10+
11+
# shellcheck disable=SC2086 # ARGS / offline_arg are intentionally word-split
12+
exec cargo ${ARGS:-} $offline_arg

‎.github/actions/cargo-fetch/action.yml‎

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -60,39 +60,22 @@ runs:
6060
GIT_TOKEN: ${{ inputs.git-token }}
6161
run: |
6262
set -euo pipefail
63-
mkdir -p "$CARGO_CACHE"
64-
# Forward env into the container (see env-include/env-exclude/env inputs);
65-
# CARGO_HOME/RUSTUP_HOME are pinned after --env-file (last-wins).
66-
ef="$(mktemp)"; trap 'rm -f "$ef"' EXIT
67-
env | awk -v inc="$ENV_INCLUDE" -v exc="$ENV_EXCLUDE" '
68-
{ name = $0; sub(/=.*/, "", name) }
69-
inc != "" && name !~ "^(" inc ")$" { next }
70-
exc != "" && name ~ "^(" exc ")$" { next }
71-
{ print }
72-
' > "$ef"
73-
if [ -n "$INPUT_ENV" ]; then printf '%s\n' "$INPUT_ENV" >> "$ef"; fi
74-
# Private git deps: forward the token as one explicit env-file line (not
75-
# via env-include) to the container's git; add-mask redacts it from logs.
63+
# shellcheck source=/dev/null
64+
source "$GITHUB_ACTION_PATH/../_lib/seal.sh"
65+
# The one networked step: OFFLINE unset → no --network=none; no target mount
66+
# (fetch only populates the cache). Forward a private-git token as one masked
67+
# env-file line (not via env-include); add-mask redacts it from logs.
7668
if [ -n "$GIT_TOKEN" ]; then
7769
printf '::add-mask::%s\n' "$GIT_TOKEN"
78-
printf 'CICD_GIT_TOKEN=%s\n' "$GIT_TOKEN" >> "$ef"
70+
export EXTRA_ENV="CICD_GIT_TOKEN=$GIT_TOKEN"
7971
fi
80-
docker run --rm \
81-
--env-file "$ef" \
82-
--user "$(id -u):$(id -g)" \
83-
-e HOME=/tmp \
84-
-e CARGO_HOME=/cache/cargo \
85-
-e RUSTUP_HOME=/usr/local/rustup \
86-
-v "$PWD:/work:ro" \
87-
-v "$PWD/$CARGO_CACHE:/cache/cargo" \
88-
-w /work \
89-
"$IMAGE" bash -c '
90-
# With a git-token, authenticate github.com fetches as x-access-token and
91-
# force cargo to fetch via the git CLI so the url.insteadOf rewrite applies
92-
# — lets cargo clone PRIVATE git dependencies. No-op for public deps.
93-
if [ -n "${CICD_GIT_TOKEN:-}" ]; then
94-
git config --global url."https://x-access-token:${CICD_GIT_TOKEN}@github.com/".insteadOf "https://github.com/"
95-
export CARGO_NET_GIT_FETCH_WITH_CLI=true
96-
fi
97-
exec cargo fetch --locked
98-
'
72+
seal_run bash -c '
73+
# With a git-token, authenticate github.com fetches as x-access-token and
74+
# force cargo to fetch via the git CLI so the url.insteadOf rewrite applies
75+
# — lets cargo clone PRIVATE git dependencies. No-op for public deps.
76+
if [ -n "${CICD_GIT_TOKEN:-}" ]; then
77+
git config --global url."https://x-access-token:${CICD_GIT_TOKEN}@github.com/".insteadOf "https://github.com/"
78+
export CARGO_NET_GIT_FETCH_WITH_CLI=true
79+
fi
80+
exec cargo fetch --locked
81+
'

‎.github/actions/check-release-readiness-docker/action.yml‎

Lines changed: 0 additions & 101 deletions
This file was deleted.

0 commit comments

Comments
 (0)