Skip to content

Commit 9ec87c6

Browse files
Claudelpcox
andauthored
feat: rust one-shot-token library (#791)
* Initial plan * feat: add rust one-shot-token library implementation Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * docs: update README for rust implementation Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * feat: update Dockerfile and entrypoint for rust build Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix(security): use multi-stage build for rust compilation Use official rust:1.77-slim Docker image in multi-stage build to avoid executing unverified rustup installer script. This mitigates supply chain attack risk during container builds. Changes: - Add rust-builder stage using rust:1.77-slim official image - Build one-shot-token library in isolated builder stage - Copy pre-built library to main stage via COPY --from=rust-builder - Remove curl/build-essential from main stage (no longer needed) - Add security comments documenting supply chain attack mitigation Addresses security review recommendation from @lpcox. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * feat(ci): add crates.io to build-test-rust network allowlist * chore(ci): regenerate build-test-rust lock file with crates.io Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: correct exit code success detection in test runner (#793) * Initial plan * fix: correct exit code success detection in test runner The awf-runner was incorrectly handling undefined exit codes from execa. When exitCode was undefined, it would set exitCode field to 0 (via || operator) but success field would evaluate undefined === 0 which is false. This caused tests to fail with the confusing message: "Expected awf to succeed, but it failed with exit code 0" Fixed by normalizing exitCode to a variable first using ?? operator, then using that normalized value for both fields. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * Initial plan (#794) Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> * fix: add Rust toolchain setup and RUSTUP_HOME support for chroot package manager tests (#797) * Initial plan * fix: add explicit toolchain to rust setup in test workflow Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * feat: add RUSTUP_HOME environment variable support for Rust toolchain Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 1d4b78d commit 9ec87c6

12 files changed

Lines changed: 647 additions & 156 deletions

File tree

.github/workflows/build-test-rust.lock.yml

Lines changed: 109 additions & 108 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/build-test-rust.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ network:
1919
- defaults
2020
- github
2121
- rust
22+
- crates.io
2223
tools:
2324
bash:
2425
- "*"

.github/workflows/test-chroot.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ jobs:
157157

158158
- name: Setup Rust
159159
uses: dtolnay/rust-toolchain@stable
160+
with:
161+
toolchain: stable
160162

161163
- name: Setup Java
162164
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v4
@@ -186,6 +188,12 @@ jobs:
186188
echo "Captured CARGO_HOME: ${CARGO_HOME}"
187189
fi
188190
191+
# Rust: RUSTUP_HOME is needed so rustc can find the toolchain
192+
if [ -n "$RUSTUP_HOME" ]; then
193+
echo "RUSTUP_HOME=${RUSTUP_HOME}" >> $GITHUB_ENV
194+
echo "Captured RUSTUP_HOME: ${RUSTUP_HOME}"
195+
fi
196+
189197
# Java: JAVA_HOME is needed so entrypoint can add $JAVA_HOME/bin to PATH
190198
# The setup-java action sets JAVA_HOME but sudo may not preserve it
191199
if [ -n "$JAVA_HOME" ]; then
@@ -210,11 +218,14 @@ jobs:
210218
echo "GOROOT: $GOROOT"
211219
echo "Ruby: $(ruby --version)"
212220
echo "Gem: $(gem --version)"
221+
echo "Bundler: $(bundle --version 2>&1 || echo 'Not installed')"
213222
echo "Rust: $(rustc --version)"
214223
echo "Cargo: $(cargo --version)"
215224
echo "CARGO_HOME: $CARGO_HOME"
225+
echo "RUSTUP_HOME: $RUSTUP_HOME"
216226
echo "Java: $(java --version 2>&1 | head -1)"
217227
echo "JAVA_HOME: $JAVA_HOME"
228+
echo "Maven: $(mvn --version 2>&1 | head -1 || echo 'Not installed')"
218229
echo "dotnet: $(dotnet --version 2>&1)"
219230
echo "DOTNET_ROOT: $DOTNET_ROOT"
220231

containers/agent/Dockerfile

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,24 @@
44
# - ghcr.io/catthehacker/ubuntu:runner-22.04: Closer to GitHub Actions runner (~2-5GB)
55
# - ghcr.io/catthehacker/ubuntu:full-22.04: Near-identical to GitHub Actions runner (~20GB compressed)
66
# Use --build-arg BASE_IMAGE=<image> to customize
7+
# NOTE: ARG declared before first FROM is global and available in all FROM statements
78
ARG BASE_IMAGE=ubuntu:22.04
9+
10+
# Multi-stage build: Use official Rust image to build one-shot-token library
11+
# SECURITY: Using official rust:1.77-slim image prevents executing unverified
12+
# scripts from the internet during build time (supply chain attack mitigation)
13+
# NOTE: Rust 1.77+ required for C string literal syntax (c"...") used in src/lib.rs
14+
FROM rust:1.77-slim AS rust-builder
15+
16+
# Copy one-shot-token source files
17+
COPY one-shot-token/Cargo.toml /tmp/one-shot-token/Cargo.toml
18+
COPY one-shot-token/src/ /tmp/one-shot-token/src/
19+
20+
# Build the one-shot-token library
21+
WORKDIR /tmp/one-shot-token
22+
RUN cargo build --release
23+
24+
# Main stage
825
FROM ${BASE_IMAGE}
926

1027
# Install required packages and Node.js 22
@@ -66,20 +83,11 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
6683
COPY pid-logger.sh /usr/local/bin/pid-logger.sh
6784
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh
6885

69-
# Build one-shot-token LD_PRELOAD library for single-use token access
86+
# Copy pre-built one-shot-token library from rust-builder stage
7087
# This prevents tokens from being read multiple times (e.g., by malicious code)
71-
COPY one-shot-token/one-shot-token.c /tmp/one-shot-token.c
72-
RUN set -eux; \
73-
BUILD_PKGS="gcc libc6-dev"; \
74-
apt-get update && \
75-
( apt-get install -y --no-install-recommends $BUILD_PKGS || \
76-
(rm -rf /var/lib/apt/lists/* && apt-get update && \
77-
apt-get install -y --no-install-recommends $BUILD_PKGS) ) && \
78-
gcc -shared -fPIC -O2 -Wall -o /usr/local/lib/one-shot-token.so /tmp/one-shot-token.c -ldl -lpthread && \
79-
rm /tmp/one-shot-token.c && \
80-
apt-get remove -y $BUILD_PKGS && \
81-
apt-get autoremove -y && \
82-
rm -rf /var/lib/apt/lists/*
88+
# SECURITY: Using multi-stage build with official Rust image avoids executing
89+
# unverified scripts from the internet during build time
90+
COPY --from=rust-builder /tmp/one-shot-token/target/release/libone_shot_token.so /usr/local/lib/one-shot-token.so
8391

8492
# Install Docker stub script that shows helpful error message
8593
# Docker-in-Docker support was removed in v0.9.1

containers/agent/entrypoint.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,16 @@ AWFEOF
292292
echo "[entrypoint] Adding CARGO_HOME/bin to PATH: ${AWF_CARGO_HOME}/bin"
293293
echo "export PATH=\"${AWF_CARGO_HOME}/bin:\$PATH\"" >> "/host${SCRIPT_FILE}"
294294
echo "export CARGO_HOME=\"${AWF_CARGO_HOME}\"" >> "/host${SCRIPT_FILE}"
295+
# Also set RUSTUP_HOME if provided (needed for rustc to find toolchain)
296+
if [ -n "${AWF_RUSTUP_HOME}" ]; then
297+
echo "[entrypoint] Setting RUSTUP_HOME: ${AWF_RUSTUP_HOME}"
298+
echo "export RUSTUP_HOME=\"${AWF_RUSTUP_HOME}\"" >> "/host${SCRIPT_FILE}"
299+
fi
300+
else
301+
# Fallback: detect Cargo from default location if CARGO_HOME not provided
302+
# This ensures Rust binaries work even when CARGO_HOME env var is not set
303+
echo "# Add Cargo bin for Rust if it exists (fallback when CARGO_HOME not provided)" >> "/host${SCRIPT_FILE}"
304+
echo "[ -d \"\$HOME/.cargo/bin\" ] && export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> "/host${SCRIPT_FILE}"
295305
fi
296306
# Add JAVA_HOME/bin to PATH if provided (for Java on GitHub Actions)
297307
# Also set LD_LIBRARY_PATH to include Java's lib directory for libjli.so
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1+
# Build output
12
*.so
3+
4+
# Rust build artifacts
5+
target/
6+
Cargo.lock
7+
8+
# C build artifacts (legacy)
9+
*.o
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "one-shot-token"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "LD_PRELOAD library for one-shot access to sensitive environment variables"
6+
license = "MIT"
7+
8+
[lib]
9+
name = "one_shot_token"
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
libc = "0.2"
14+
once_cell = "1.19"
15+
16+
[profile.release]
17+
opt-level = 2
18+
lto = true
19+
strip = true

containers/agent/one-shot-token/README.md

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,22 +158,22 @@ In chroot mode, the library must be accessible from within the chroot (host file
158158

159159
### In Docker (automatic)
160160

161-
The Dockerfile compiles the library during image build:
161+
The Dockerfile compiles the Rust library during image build:
162162

163163
```dockerfile
164-
RUN gcc -shared -fPIC -O2 -Wall \
165-
-o /usr/local/lib/one-shot-token.so \
166-
/tmp/one-shot-token.c \
167-
-ldl -lpthread
164+
RUN cargo build --release && \
165+
cp target/release/libone_shot_token.so /usr/local/lib/one-shot-token.so
168166
```
169167

170168
### Locally (for testing)
171169

170+
Requires Rust toolchain (install via [rustup](https://rustup.rs/)):
171+
172172
```bash
173173
./build.sh
174174
```
175175

176-
This produces `one-shot-token.so` in the current directory.
176+
This builds `target/release/libone_shot_token.so` and creates a symlink `one-shot-token.so` for backwards compatibility.
177177

178178
## Testing
179179

@@ -274,6 +274,20 @@ Note: The `AWF_ONE_SHOT_TOKENS` variable must be exported before running `awf` s
274274
- **In-process getenv() calls**: Since values are cached, any code in the same process can still call `getenv()` and get the cached token
275275
- **Static linking**: Programs statically linked with libc bypass LD_PRELOAD
276276
- **Direct syscalls**: Code that reads `/proc/self/environ` directly (without getenv) bypasses this protection
277+
- **Task-level /proc exposure**: `/proc/PID/task/TID/environ` may still expose tokens even after `unsetenv()`. The library checks and logs warnings about this exposure.
278+
279+
### Environment Verification
280+
281+
After calling `unsetenv()` to clear tokens, the library automatically verifies whether the token was successfully removed by directly checking the process's environment pointer. This works correctly in both regular and chroot modes.
282+
283+
**Log messages:**
284+
- `INFO: Token <name> cleared from process environment` - Token successfully cleared (✓ secure)
285+
- `WARNING: Token <name> still exposed in process environment` - Token still visible (⚠ security concern)
286+
- `INFO: Token <name> cleared (environ is null)` - Environment pointer is null
287+
288+
This verification runs automatically after `unsetenv()` on first access to each sensitive token and helps identify potential security issues with environment exposure.
289+
290+
**Note on chroot mode:** The verification uses the process's `environ` pointer directly rather than reading from `/proc/self/environ`. This is necessary because in chroot mode, `/proc` may be bind-mounted from the host and show stale environment data.
277291

278292
### Defense in Depth
279293

@@ -285,12 +299,13 @@ This library is one layer in AWF's security model:
285299

286300
## Limitations
287301

288-
- **x86_64 Linux only**: The library is compiled for x86_64 Ubuntu
302+
- **Linux only**: The library is compiled for Linux (x86_64 and potentially other architectures via Rust cross-compilation)
289303
- **glibc programs only**: Programs using musl libc or statically linked programs are not affected
290304
- **Single process**: Child processes inherit the LD_PRELOAD but have their own token state and cache (each starts fresh)
291305

292306
## Files
293307

294-
- `one-shot-token.c` - Library source code
308+
- `src/lib.rs` - Library source code (Rust)
309+
- `Cargo.toml` - Rust package configuration
295310
- `build.sh` - Local build script
296311
- `README.md` - This documentation
Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
#!/bin/bash
22
# Build the one-shot-token LD_PRELOAD library
3-
# This script compiles the shared library for x86_64 Ubuntu
3+
# This script compiles the Rust shared library
44

55
set -e
66

77
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8-
SOURCE_FILE="${SCRIPT_DIR}/one-shot-token.c"
9-
OUTPUT_FILE="${SCRIPT_DIR}/one-shot-token.so"
10-
11-
echo "[build] Compiling one-shot-token.so..."
12-
13-
# Compile as a shared library with position-independent code
14-
# -shared: create a shared library
15-
# -fPIC: position-independent code (required for shared libs)
16-
# -ldl: link with libdl for dlsym
17-
# -lpthread: link with pthread for mutex
18-
# -O2: optimize for performance
19-
# -Wall -Wextra: enable warnings
20-
gcc -shared -fPIC \
21-
-O2 -Wall -Wextra \
22-
-o "${OUTPUT_FILE}" \
23-
"${SOURCE_FILE}" \
24-
-ldl -lpthread
25-
26-
echo "[build] Successfully built: ${OUTPUT_FILE}"
8+
LINK_FILE="${SCRIPT_DIR}/one-shot-token.so"
9+
10+
echo "[build] Building one-shot-token with Cargo..."
11+
12+
cd "${SCRIPT_DIR}"
13+
14+
# Build the release version
15+
cargo build --release
16+
17+
# Determine the output file based on platform
18+
if [[ "$(uname)" == "Darwin" ]]; then
19+
OUTPUT_FILE="${SCRIPT_DIR}/target/release/libone_shot_token.dylib"
20+
echo "[build] Successfully built: ${OUTPUT_FILE} (macOS)"
21+
else
22+
OUTPUT_FILE="${SCRIPT_DIR}/target/release/libone_shot_token.so"
23+
echo "[build] Successfully built: ${OUTPUT_FILE}"
24+
25+
# Create symlink for backwards compatibility (Linux only)
26+
if [[ -L "${LINK_FILE}" ]]; then
27+
rm "${LINK_FILE}"
28+
fi
29+
ln -sf "target/release/libone_shot_token.so" "${LINK_FILE}"
30+
echo "[build] Created symlink: ${LINK_FILE} -> target/release/libone_shot_token.so"
31+
fi
2732

2833
# Verify it's a valid shared library
29-
if file "${OUTPUT_FILE}" | grep -q "shared object"; then
30-
echo "[build] Verified: valid shared object"
34+
if file "${OUTPUT_FILE}" | grep -qE "shared object|dynamically linked"; then
35+
echo "[build] Verified: valid shared library"
3136
else
32-
echo "[build] ERROR: Output is not a valid shared object"
37+
echo "[build] ERROR: Output is not a valid shared library"
3338
exit 1
3439
fi

0 commit comments

Comments
 (0)