Sign commits made in the sandbox
The sandbox held no signing material, so its commits arrived unverified and a branch rule requiring signatures rejected them outright. AI_SBX_SIGNING_KEY copies an SSH signing key into the sandbox and points both git and jj at it. The private half genuinely lands in the sandbox, which is why this is opt-in and documented as signing-only: an agent that can read the key can sign as you. A signing key grants no repository access and is revocable on its own, so the exposure is forged attestation rather than reach. Forwarding an agent socket would avoid the copy, but a socket passed over virtiofs is visible and unconnectable from the guest, and the TCP workaround is a worse trade. Setup refuses a passphrase-protected key rather than letting the failure surface on the agent's first commit, and writes an allowed_signers entry so the sandbox can verify what it just signed. jj is configured through conf.d, which is read after config.toml and so overrides the host key path a copied dotfile carries.
This commit is contained in:
@@ -10,6 +10,7 @@ DEFAULT_TEMPLATE="${AI_SBX_TEMPLATE:-}"
|
||||
DEFAULT_TOOLS="${AI_SBX_TOOLS:-bun}"
|
||||
DEFAULT_NETWORK="${AI_SBX_NETWORK:-}"
|
||||
DEFAULT_LAUNCH="${AI_SBX_LAUNCH:-agent}"
|
||||
DEFAULT_SIGNING_KEY="${AI_SBX_SIGNING_KEY:-}"
|
||||
DEFAULT_DOTFILES="${AI_SBX_DOTFILES:-}"
|
||||
|
||||
# Rendered dotfiles are checked for these before the archive enters the sandbox.
|
||||
@@ -121,6 +122,13 @@ AI_SBX_TOOLS lists mise tools installed globally in the sandbox, defaulting
|
||||
to bun because several Claude plugins run their hooks under it. Set it to an
|
||||
empty string to install none.
|
||||
|
||||
AI_SBX_SIGNING_KEY is the path to an SSH signing key on the host. Its private
|
||||
half is copied into the sandbox, so the agent can sign as you: use a key that
|
||||
signs and nothing else, and never an authentication key. Unset, the default,
|
||||
leaves the sandbox unable to sign and its commits arrive unverified. The key
|
||||
must not be passphrase-protected, because nothing in the sandbox can answer
|
||||
the prompt. git and jj are both pointed at it.
|
||||
|
||||
Setup and run install mise in the sandbox and resolve the repository's
|
||||
pinned tools, so the agent runs the same versions you do. A personal
|
||||
mise config that must stay out of the repository goes in the per-repository
|
||||
@@ -1123,6 +1131,85 @@ install_sandbox_git_https() {
|
||||
"$SANDBOX_NAME" >&2
|
||||
}
|
||||
|
||||
# The private half genuinely lands in the sandbox, which is why the key is
|
||||
# opt-in and must be signing-only: an agent that can read it can sign as you.
|
||||
# A signing key is separately revocable and grants no repository access, so the
|
||||
# damage is forged attestation rather than reach.
|
||||
install_sandbox_signing_key() {
|
||||
[[ -n "$DEFAULT_SIGNING_KEY" ]] || return 0
|
||||
|
||||
local private="${DEFAULT_SIGNING_KEY/#\~/$HOME}"
|
||||
local public="$private.pub"
|
||||
|
||||
[[ -f "$private" ]] ||
|
||||
die "AI_SBX_SIGNING_KEY does not exist: $private"
|
||||
|
||||
[[ -f "$public" ]] ||
|
||||
die "No public half beside $private. SSH signing needs both, and git names the signing key by its .pub."
|
||||
|
||||
ssh-keygen -y -P '' -f "$private" >/dev/null 2>&1 ||
|
||||
die "$private is passphrase-protected. Nothing in the sandbox can answer the prompt, so every commit would fail at the moment of signing. Use a dedicated signing key with no passphrase."
|
||||
|
||||
local principal
|
||||
principal="$(git -C "$REPO_ROOT" config user.email)" ||
|
||||
die "The repository has no user.email, so signatures could not be attributed to a principal."
|
||||
|
||||
local sandbox_home
|
||||
# shellcheck disable=SC2016
|
||||
sandbox_home="$(sbx exec "$SANDBOX_NAME" bash -c 'printf %s "$HOME"')"
|
||||
|
||||
[[ -n "$sandbox_home" ]] ||
|
||||
die "Could not determine the sandbox home directory."
|
||||
|
||||
local staging
|
||||
staging="$(mktemp -d)"
|
||||
trap 'rm -rf "$staging"' RETURN
|
||||
|
||||
local name
|
||||
name="$(basename "$private")"
|
||||
|
||||
mkdir -p "$staging/.ssh"
|
||||
install -m 600 "$private" "$staging/.ssh/$name"
|
||||
install -m 644 "$public" "$staging/.ssh/$name.pub"
|
||||
|
||||
# Without a principal mapping git reports "No principal matched" for the
|
||||
# signatures it just produced, so the sandbox cannot verify its own commits.
|
||||
printf '%s %s\n' "$principal" "$(cat "$public")" \
|
||||
>"$staging/.ssh/allowed_signers"
|
||||
|
||||
tar -C "$staging" -cf - .ssh |
|
||||
sbx exec -i "$SANDBOX_NAME" tar -x -C "$sandbox_home" ||
|
||||
die "Could not copy the signing key into $SANDBOX_NAME."
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
sbx exec "$SANDBOX_NAME" bash -c '
|
||||
set -e
|
||||
name="$1"
|
||||
|
||||
chmod 700 "$HOME/.ssh"
|
||||
chmod 600 "$HOME/.ssh/$name"
|
||||
chmod 644 "$HOME/.ssh/$name.pub" "$HOME/.ssh/allowed_signers"
|
||||
|
||||
git config --global gpg.format ssh
|
||||
git config --global user.signingkey "$HOME/.ssh/$name.pub"
|
||||
git config --global commit.gpgsign true
|
||||
git config --global tag.gpgsign true
|
||||
git config --global gpg.ssh.allowedSignersFile "$HOME/.ssh/allowed_signers"
|
||||
|
||||
# jj reads conf.d after config.toml, so the host key path a copied
|
||||
# dotfile carries is overridden without editing a file chezmoi owns.
|
||||
mkdir -p "$HOME/.config/jj/conf.d"
|
||||
cat >"$HOME/.config/jj/conf.d/10-ai-sbx-signing.toml" <<EOF
|
||||
[signing]
|
||||
backend = "ssh"
|
||||
key = "$HOME/.ssh/$name.pub"
|
||||
EOF
|
||||
' _ "$name" </dev/null ||
|
||||
die "Could not configure commit signing in $SANDBOX_NAME."
|
||||
|
||||
printf 'Installed the signing key %s into %s.\n' "$name" "$SANDBOX_NAME"
|
||||
}
|
||||
|
||||
validate_launch_mode() {
|
||||
case "$1" in
|
||||
agent | tmux) ;;
|
||||
@@ -1409,6 +1496,8 @@ setup_command() {
|
||||
# otherwise land on top of the rewrite.
|
||||
install_sandbox_git_https
|
||||
|
||||
install_sandbox_signing_key
|
||||
|
||||
install_sandbox_network
|
||||
|
||||
install_sandbox_secrets
|
||||
@@ -1463,6 +1552,8 @@ config_command() {
|
||||
|
||||
install_sandbox_git_https
|
||||
|
||||
install_sandbox_signing_key
|
||||
|
||||
install_sandbox_network
|
||||
|
||||
install_sandbox_secrets
|
||||
|
||||
Reference in New Issue
Block a user