scripts/cosign-keygen.sh generates a cosign keypair and emits a SOPS-encrypted Kubernetes Secret manifest containing the private key and password. Defenses: - Keypair is generated inside /dev/shm (tmpfs) when available; falls back to /tmp with shred-on-exit. Trap fires on EXIT/INT/TERM/HUP. - Password read with `read -rs` (no terminal echo). Minimum 12 chars. Unset immediately after the Secret YAML is built. - The private key + password are SOPS-encrypted in place via --encrypted-regex; the public key remains plaintext (intended for commit alongside the encrypted Secret). - Neither cosign nor sops output keys on success paths; `set -x` is never enabled. - Refuses to overwrite an existing output file. Companion changes to keep accidental secret leakage out of git: - .gitignore: ignore secrets/* by default, allow only *.enc.yaml, *.enc.yml, and cosign.pub through. - secrets/.gitkeep: anchor the directory so the gitignore rules apply to a real path. Pairs with the conditional cosign sign step in .forgejo/workflows/release.yml. Refs forgejo-mcp-51l.
161 lines
5.3 KiB
Bash
Executable file
161 lines
5.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# cosign-keygen.sh — generate a cosign keypair and emit a SOPS-encrypted
|
|
# Kubernetes Secret manifest. The private key and password never touch
|
|
# unencrypted disk outside a tmpfs working directory and are never echoed
|
|
# to stdout/stderr.
|
|
#
|
|
# Usage:
|
|
# scripts/cosign-keygen.sh [namespace] [secret-name] [output-file]
|
|
#
|
|
# Defaults:
|
|
# namespace = forgejo-mcp
|
|
# secret-name = cosign-signing-key
|
|
# output-file = secrets/cosign-signing-key.enc.yaml
|
|
#
|
|
# Prerequisites:
|
|
# - cosign (https://github.com/sigstore/cosign)
|
|
# - sops (https://github.com/getsops/sops) with a working key in .sops.yaml
|
|
# - A POSIX-ish shell with /dev/shm OR /tmp + shred(1)
|
|
#
|
|
# Output:
|
|
# - $output-file → SOPS-encrypted k8s Secret (cosign.key + cosign.password fields encrypted)
|
|
# - dirname($out)/cosign.pub → plaintext public key (safe + intended to commit)
|
|
#
|
|
# Security guarantees:
|
|
# - Private key + password live only in /dev/shm (RAM) when available;
|
|
# fallback to /tmp with shred-on-exit (best effort).
|
|
# - Password is read with `read -rs` (no terminal echo) and unset
|
|
# immediately after the Secret YAML is built.
|
|
# - Neither sops nor cosign output prints key material on success paths.
|
|
# - `set -x` is never enabled. Do not add it.
|
|
|
|
set -euo pipefail
|
|
|
|
NAMESPACE="${1:-forgejo-mcp}"
|
|
SECRET_NAME="${2:-cosign-signing-key}"
|
|
OUT_FILE="${3:-secrets/cosign-signing-key.enc.yaml}"
|
|
|
|
# --- preflight ----------------------------------------------------------------
|
|
|
|
need() { command -v "$1" >/dev/null 2>&1 || { echo "missing dependency: $1" >&2; exit 2; }; }
|
|
need cosign
|
|
need sops
|
|
need base64
|
|
need sed
|
|
need mktemp
|
|
|
|
if [ -f "$OUT_FILE" ]; then
|
|
printf 'Refuse to overwrite existing file: %s\n' "$OUT_FILE" >&2
|
|
printf 'Move/delete it first if you really mean to regenerate.\n' >&2
|
|
exit 3
|
|
fi
|
|
|
|
# --- tmpfs work dir -----------------------------------------------------------
|
|
|
|
if [ -d /dev/shm ] && [ -w /dev/shm ]; then
|
|
WORKDIR=$(mktemp -d /dev/shm/cosign-keygen.XXXXXXXX)
|
|
RAM_BACKED=1
|
|
else
|
|
WORKDIR=$(mktemp -d)
|
|
RAM_BACKED=0
|
|
printf 'warning: /dev/shm unavailable; using %s (disk-backed). Will shred on exit.\n' "$WORKDIR" >&2
|
|
fi
|
|
chmod 700 "$WORKDIR"
|
|
|
|
cleanup() {
|
|
if [ -d "$WORKDIR" ]; then
|
|
if [ "$RAM_BACKED" = "0" ] && command -v shred >/dev/null 2>&1; then
|
|
find "$WORKDIR" -type f -exec shred -uz {} + 2>/dev/null || true
|
|
fi
|
|
rm -rf "$WORKDIR"
|
|
fi
|
|
unset COSIGN_PASSWORD COSIGN_PASSWORD_CONFIRM 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT INT TERM HUP
|
|
|
|
# --- password (no echo) -------------------------------------------------------
|
|
|
|
printf 'Enter cosign key password (will not echo, min 12 chars): ' >&2
|
|
IFS= read -rs COSIGN_PASSWORD
|
|
printf '\n' >&2
|
|
if [ "${#COSIGN_PASSWORD}" -lt 12 ]; then
|
|
printf 'password too short (need >=12 chars)\n' >&2
|
|
exit 4
|
|
fi
|
|
printf 'Confirm: ' >&2
|
|
IFS= read -rs COSIGN_PASSWORD_CONFIRM
|
|
printf '\n' >&2
|
|
if [ "$COSIGN_PASSWORD" != "$COSIGN_PASSWORD_CONFIRM" ]; then
|
|
printf 'passwords do not match\n' >&2
|
|
exit 5
|
|
fi
|
|
unset COSIGN_PASSWORD_CONFIRM
|
|
export COSIGN_PASSWORD
|
|
|
|
# --- generate keypair into tmpfs (cosign writes cosign.key + cosign.pub) ------
|
|
|
|
# Redirect cosign's stdout: only the "Private key written to ..." path string
|
|
# would be shown, but suppress it entirely as defense-in-depth.
|
|
(cd "$WORKDIR" && cosign generate-key-pair) >/dev/null
|
|
|
|
if [ ! -s "$WORKDIR/cosign.key" ] || [ ! -s "$WORKDIR/cosign.pub" ]; then
|
|
printf 'cosign did not produce expected files\n' >&2
|
|
exit 6
|
|
fi
|
|
chmod 600 "$WORKDIR/cosign.key"
|
|
|
|
# --- build k8s Secret YAML in tmpfs ------------------------------------------
|
|
|
|
# Use stringData so kubectl handles the values verbatim. SOPS encrypts the
|
|
# sensitive fields (cosign.key + cosign.password) in place via --encrypted-regex.
|
|
{
|
|
printf 'apiVersion: v1\n'
|
|
printf 'kind: Secret\n'
|
|
printf 'metadata:\n'
|
|
printf ' name: %s\n' "$SECRET_NAME"
|
|
printf ' namespace: %s\n' "$NAMESPACE"
|
|
printf 'type: Opaque\n'
|
|
printf 'stringData:\n'
|
|
printf ' cosign.key: |\n'
|
|
sed 's/^/ /' "$WORKDIR/cosign.key"
|
|
printf ' cosign.password: %s\n' "$(printf '%s' "$COSIGN_PASSWORD" | sed 's/"/\\"/g; s/^/"/; s/$/"/')"
|
|
printf ' cosign.pub: |\n'
|
|
sed 's/^/ /' "$WORKDIR/cosign.pub"
|
|
} > "$WORKDIR/secret.yaml"
|
|
|
|
unset COSIGN_PASSWORD
|
|
|
|
# --- SOPS encrypt directly to destination -------------------------------------
|
|
|
|
OUT_DIR=$(dirname "$OUT_FILE")
|
|
mkdir -p "$OUT_DIR"
|
|
|
|
sops --encrypt \
|
|
--input-type yaml \
|
|
--output-type yaml \
|
|
--encrypted-regex '^(cosign\.key|cosign\.password)$' \
|
|
"$WORKDIR/secret.yaml" > "$OUT_FILE"
|
|
|
|
# Public key is intended to be committed and verified against. Emit alongside.
|
|
cp "$WORKDIR/cosign.pub" "$OUT_DIR/cosign.pub"
|
|
chmod 644 "$OUT_DIR/cosign.pub" "$OUT_FILE"
|
|
|
|
# --- success message (no key material) ----------------------------------------
|
|
|
|
cat <<MSG
|
|
done.
|
|
|
|
encrypted Secret : $OUT_FILE
|
|
public key : $OUT_DIR/cosign.pub (commit this)
|
|
|
|
next steps:
|
|
1. verify decrypt round-trip:
|
|
sops --decrypt $OUT_FILE | head -10
|
|
2. apply to cluster (if that's your delivery path):
|
|
sops exec-file $OUT_FILE 'kubectl apply -f {}'
|
|
3. for Codeberg Actions, mirror the two fields into repo secrets:
|
|
sops --decrypt $OUT_FILE | yq -r '.stringData["cosign.key"]' → COSIGN_PRIVATE_KEY
|
|
sops --decrypt $OUT_FILE | yq -r '.stringData["cosign.password"]' → COSIGN_PASSWORD
|
|
(do this in a private terminal; do not redirect to disk.)
|
|
MSG
|