Local SOPS-Encrypted Repository Secrets and Retirement of Modal Secret Collections¶
Previously, repository cloning credentials (GH_REPO, GH_TOKEN, and optional DOTENV_PRIVATE_KEY) were managed as cloud-hosted Modal secret collections passed via --secret-collection (ADR 0005). This required maintaining separate Modal secret objects in the Modal cloud dashboard for each repository and coupled sandbox launching with remote secret collection provisioning. We decided to retire --secret-collection and all repository-level Modal secret collections in favor of local SOPS-encrypted Repository Fetch Secrets stored version-controlled at secrets/<secret-name>/fetch.env, selected via -s or --secret <secret-name>. Sandbox runners bake the encrypted secret file into the container image at /root/secrets/fetch.env, mount the Shared Workspace Secret share0 unconditionally across all sandboxes, and decrypt the credentials ephemerally into memory in shared/startup.sh at container boot using AGE_PRIVATE_KEY before immediately purging /root/secrets/fetch.env from disk and unsetting GH_TOKEN from memory prior to user session handoff.
Considered Options¶
- Host-side SOPS decryption via
modal.Secret.from_dict: Decryptsecrets/<secret-name>/fetch.envon the host workstation runningsandbox/main.pyusing localsops decrypt, and inject the resulting environment variables as an ephemeral Modal secret. Rejected because it requires every developer workstation and CI/CD agent to haveAGE_PRIVATE_KEYstored in local filesystem key files (~/.config/sops/age/keys.txt), breaking decoupled runner execution (ADR 0003). - Retain
--secret-collectionalongside--secret: Keep--secret-collectionas a backwards-compatible alias that looks up Modal secret collections. Rejected because supporting two divergent secret distribution models creates operational ambiguity and undermines the unified local SOPS security architecture. - In-sandbox SOPS decryption with unconditional
share0mounting: Bakesecrets/<secret-name>/fetch.envinto the container image at/root/secrets/fetch.env, mountshare0unconditionally across all sandboxes to supplyAGE_PRIVATE_KEY, strictly decrypt credentials in memory withinshared/startup.shprior togit clone, immediately remove/root/secrets/fetch.envfrom the filesystem, and unsetGH_TOKENbefore interactive shell access. Accepted because it maintains full zero-leakage distribution, requires zero host-side private keys, unifies credential management under the SOPS + Age architecture, and eliminates remote Modal secret creation friction.
Consequences¶
--secret-collectionis completely removed;-sand--secret <secret-name>are the canonical CLI arguments insandbox/main.py.sandbox/main.pyfails fast at argument parsing ifsecrets/<secret-name>/fetch.envis missing on the local filesystem.sandbox/main.pybakessecrets/<secret-name>/fetch.envinto/root/secrets/fetch.envfor the target sandbox image and unconditionally mountsmodal.Secret.from_name("share0").shared/startup.shstrictly decrypts/root/secrets/fetch.envusingSOPS_AGE_KEY="$AGE_PRIVATE_KEY" sops decryptin Step 1 beforegit clone. If decryption fails orAGE_PRIVATE_KEYis missing, container startup terminates immediately with exit code 1.shared/startup.shpurges/root/secrets/fetch.envfrom disk immediately after reading into memory, and unsetsGH_TOKENand temporary decryption variables from process memory before server startup and interactive session sleep.- All sandboxes require the centralized Modal secret
share0to exist in the Modal workspace.