Skip to content
Merged
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
217 changes: 109 additions & 108 deletions .github/workflows/build-test-rust.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/build-test-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ network:
- defaults
- github
- rust
- crates.io
tools:
bash:
- "*"
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/test-chroot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ jobs:

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dtolnay/rust-toolchain action reference is missing a version pin or SHA. While the with: toolchain: stable configuration is present, the action itself should be pinned to a specific commit SHA for security and reproducibility, consistent with the convention used for other actions in this workflow (e.g., actions/checkout, actions/setup-node, etc. are all pinned to SHAs).

Suggested change
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4f6abf093ee4c743c3c1e811ce0cfe4631d14d1f # stable

Copilot uses AI. Check for mistakes.
with:
toolchain: stable

- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v4
Expand Down Expand Up @@ -186,6 +188,12 @@ jobs:
echo "Captured CARGO_HOME: ${CARGO_HOME}"
fi

# Rust: RUSTUP_HOME is needed so rustc can find the toolchain
if [ -n "$RUSTUP_HOME" ]; then
echo "RUSTUP_HOME=${RUSTUP_HOME}" >> $GITHUB_ENV
echo "Captured RUSTUP_HOME: ${RUSTUP_HOME}"
fi

# Java: JAVA_HOME is needed so entrypoint can add $JAVA_HOME/bin to PATH
# The setup-java action sets JAVA_HOME but sudo may not preserve it
if [ -n "$JAVA_HOME" ]; then
Expand All @@ -210,11 +218,14 @@ jobs:
echo "GOROOT: $GOROOT"
echo "Ruby: $(ruby --version)"
echo "Gem: $(gem --version)"
echo "Bundler: $(bundle --version 2>&1 || echo 'Not installed')"
echo "Rust: $(rustc --version)"
echo "Cargo: $(cargo --version)"
echo "CARGO_HOME: $CARGO_HOME"
echo "RUSTUP_HOME: $RUSTUP_HOME"
echo "Java: $(java --version 2>&1 | head -1)"
echo "JAVA_HOME: $JAVA_HOME"
echo "Maven: $(mvn --version 2>&1 | head -1 || echo 'Not installed')"
echo "dotnet: $(dotnet --version 2>&1)"
echo "DOTNET_ROOT: $DOTNET_ROOT"

Expand Down
34 changes: 21 additions & 13 deletions containers/agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,24 @@
# - ghcr.io/catthehacker/ubuntu:runner-22.04: Closer to GitHub Actions runner (~2-5GB)
# - ghcr.io/catthehacker/ubuntu:full-22.04: Near-identical to GitHub Actions runner (~20GB compressed)
# Use --build-arg BASE_IMAGE=<image> to customize
# NOTE: ARG declared before first FROM is global and available in all FROM statements
ARG BASE_IMAGE=ubuntu:22.04

# Multi-stage build: Use official Rust image to build one-shot-token library
# SECURITY: Using official rust:1.77-slim image prevents executing unverified
# scripts from the internet during build time (supply chain attack mitigation)
# NOTE: Rust 1.77+ required for C string literal syntax (c"...") used in src/lib.rs
FROM rust:1.77-slim AS rust-builder

# Copy one-shot-token source files
COPY one-shot-token/Cargo.toml /tmp/one-shot-token/Cargo.toml
COPY one-shot-token/src/ /tmp/one-shot-token/src/

# Build the one-shot-token library
WORKDIR /tmp/one-shot-token
RUN cargo build --release

# Main stage
FROM ${BASE_IMAGE}

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

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

# Install Docker stub script that shows helpful error message
# Docker-in-Docker support was removed in v0.9.1
Expand Down
10 changes: 10 additions & 0 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,16 @@ AWFEOF
echo "[entrypoint] Adding CARGO_HOME/bin to PATH: ${AWF_CARGO_HOME}/bin"
echo "export PATH=\"${AWF_CARGO_HOME}/bin:\$PATH\"" >> "/host${SCRIPT_FILE}"
echo "export CARGO_HOME=\"${AWF_CARGO_HOME}\"" >> "/host${SCRIPT_FILE}"
# Also set RUSTUP_HOME if provided (needed for rustc to find toolchain)
if [ -n "${AWF_RUSTUP_HOME}" ]; then
echo "[entrypoint] Setting RUSTUP_HOME: ${AWF_RUSTUP_HOME}"
echo "export RUSTUP_HOME=\"${AWF_RUSTUP_HOME}\"" >> "/host${SCRIPT_FILE}"
fi
else
# Fallback: detect Cargo from default location if CARGO_HOME not provided
# This ensures Rust binaries work even when CARGO_HOME env var is not set
echo "# Add Cargo bin for Rust if it exists (fallback when CARGO_HOME not provided)" >> "/host${SCRIPT_FILE}"
echo "[ -d \"\$HOME/.cargo/bin\" ] && export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> "/host${SCRIPT_FILE}"
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic adds $HOME/.cargo/bin to PATH when CARGO_HOME is not provided. However, this fallback doesn't set RUSTUP_HOME, which means rustc and cargo may not find the Rust toolchain even if the binaries are in PATH. Consider adding a similar fallback for RUSTUP_HOME (e.g., [ -d "$HOME/.rustup" ] && export RUSTUP_HOME="$HOME/.rustup") to ensure Rust tools work correctly in the fallback scenario.

Suggested change
echo "[ -d \"\$HOME/.cargo/bin\" ] && export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> "/host${SCRIPT_FILE}"
echo "[ -d \"\$HOME/.cargo/bin\" ] && export PATH=\"\$HOME/.cargo/bin:\$PATH\"" >> "/host${SCRIPT_FILE}"
echo "[ -d \"\$HOME/.rustup\" ] && export RUSTUP_HOME=\"\$HOME/.rustup\"" >> "/host${SCRIPT_FILE}"

Copilot uses AI. Check for mistakes.
fi
# Add JAVA_HOME/bin to PATH if provided (for Java on GitHub Actions)
# Also set LD_LIBRARY_PATH to include Java's lib directory for libjli.so
Expand Down
8 changes: 8 additions & 0 deletions containers/agent/one-shot-token/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
# Build output
*.so

# Rust build artifacts
target/
Cargo.lock
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cargo.lock is excluded from version control for this library. While this is appropriate for Rust library crates (cdylib), the project uses the library in a Docker multi-stage build. For reproducible builds and supply chain security, consider committing Cargo.lock. This ensures the exact same dependency versions are used across all builds and makes it easier to track dependency updates for security vulnerabilities.

Suggested change
Cargo.lock

Copilot uses AI. Check for mistakes.

# C build artifacts (legacy)
*.o
19 changes: 19 additions & 0 deletions containers/agent/one-shot-token/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "one-shot-token"
version = "0.1.0"
edition = "2021"
description = "LD_PRELOAD library for one-shot access to sensitive environment variables"
license = "MIT"

[lib]
name = "one_shot_token"
crate-type = ["cdylib"]

[dependencies]
libc = "0.2"
once_cell = "1.19"
Comment on lines +13 to +14
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Cargo.toml specifies libc = "0.2" and once_cell = "1.19" without patch versions. While this is common practice for Rust libraries, consider pinning to more specific versions (e.g., "0.2.150" and "1.19.0") for reproducible builds, especially since this is a security-critical component that intercepts environment variable access. This would make builds more deterministic and easier to audit for security vulnerabilities.

Suggested change
libc = "0.2"
once_cell = "1.19"
libc = "0.2.150"
once_cell = "1.19.0"

Copilot uses AI. Check for mistakes.

[profile.release]
opt-level = 2
lto = true
strip = true
31 changes: 23 additions & 8 deletions containers/agent/one-shot-token/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,22 +158,22 @@ In chroot mode, the library must be accessible from within the chroot (host file

### In Docker (automatic)

The Dockerfile compiles the library during image build:
The Dockerfile compiles the Rust library during image build:

```dockerfile
RUN gcc -shared -fPIC -O2 -Wall \
-o /usr/local/lib/one-shot-token.so \
/tmp/one-shot-token.c \
-ldl -lpthread
RUN cargo build --release && \
cp target/release/libone_shot_token.so /usr/local/lib/one-shot-token.so
```

### Locally (for testing)

Requires Rust toolchain (install via [rustup](https://rustup.rs/)):

```bash
./build.sh
```

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

## Testing

Expand Down Expand Up @@ -274,6 +274,20 @@ Note: The `AWF_ONE_SHOT_TOKENS` variable must be exported before running `awf` s
- **In-process getenv() calls**: Since values are cached, any code in the same process can still call `getenv()` and get the cached token
- **Static linking**: Programs statically linked with libc bypass LD_PRELOAD
- **Direct syscalls**: Code that reads `/proc/self/environ` directly (without getenv) bypasses this protection
- **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.

### Environment Verification

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.

**Log messages:**
- `INFO: Token <name> cleared from process environment` - Token successfully cleared (✓ secure)
- `WARNING: Token <name> still exposed in process environment` - Token still visible (⚠ security concern)
- `INFO: Token <name> cleared (environ is null)` - Environment pointer is null

This verification runs automatically after `unsetenv()` on first access to each sensitive token and helps identify potential security issues with environment exposure.

**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.

### Defense in Depth

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

## Limitations

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

## Files

- `one-shot-token.c` - Library source code
- `src/lib.rs` - Library source code (Rust)
- `Cargo.toml` - Rust package configuration
- `build.sh` - Local build script
- `README.md` - This documentation
51 changes: 28 additions & 23 deletions containers/agent/one-shot-token/build.sh
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
#!/bin/bash
# Build the one-shot-token LD_PRELOAD library
# This script compiles the shared library for x86_64 Ubuntu
# This script compiles the Rust shared library

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_FILE="${SCRIPT_DIR}/one-shot-token.c"
OUTPUT_FILE="${SCRIPT_DIR}/one-shot-token.so"

echo "[build] Compiling one-shot-token.so..."

# Compile as a shared library with position-independent code
# -shared: create a shared library
# -fPIC: position-independent code (required for shared libs)
# -ldl: link with libdl for dlsym
# -lpthread: link with pthread for mutex
# -O2: optimize for performance
# -Wall -Wextra: enable warnings
gcc -shared -fPIC \
-O2 -Wall -Wextra \
-o "${OUTPUT_FILE}" \
"${SOURCE_FILE}" \
-ldl -lpthread

echo "[build] Successfully built: ${OUTPUT_FILE}"
LINK_FILE="${SCRIPT_DIR}/one-shot-token.so"

echo "[build] Building one-shot-token with Cargo..."

cd "${SCRIPT_DIR}"

# Build the release version
cargo build --release

# Determine the output file based on platform
if [[ "$(uname)" == "Darwin" ]]; then
OUTPUT_FILE="${SCRIPT_DIR}/target/release/libone_shot_token.dylib"
echo "[build] Successfully built: ${OUTPUT_FILE} (macOS)"
else
OUTPUT_FILE="${SCRIPT_DIR}/target/release/libone_shot_token.so"
echo "[build] Successfully built: ${OUTPUT_FILE}"

# Create symlink for backwards compatibility (Linux only)
if [[ -L "${LINK_FILE}" ]]; then
rm "${LINK_FILE}"
fi
ln -sf "target/release/libone_shot_token.so" "${LINK_FILE}"
echo "[build] Created symlink: ${LINK_FILE} -> target/release/libone_shot_token.so"
fi

# Verify it's a valid shared library
if file "${OUTPUT_FILE}" | grep -q "shared object"; then
echo "[build] Verified: valid shared object"
if file "${OUTPUT_FILE}" | grep -qE "shared object|dynamically linked"; then
echo "[build] Verified: valid shared library"
else
echo "[build] ERROR: Output is not a valid shared object"
echo "[build] ERROR: Output is not a valid shared library"
exit 1
fi
Loading
Loading