ci: switch deploy secrets to age key workflow
This commit is contained in:
parent
d6a9af8f1e
commit
4dbd831d0b
10 changed files with 186 additions and 30 deletions
|
|
@ -11,25 +11,76 @@ concurrency:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
# Use Codeberg global hosted runners by label.
|
|
||||||
# Available labels in this repo are: codeberg-tiny, codeberg-small, codeberg-medium.
|
|
||||||
runs-on: codeberg-medium
|
runs-on: codeberg-medium
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy website + worker
|
- name: Checkout
|
||||||
env:
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
with:
|
||||||
CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
|
token: ${{ github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
lfs: false
|
||||||
|
|
||||||
|
- name: Bootstrap runner deps
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [[ ! -f "./scripts/deploy-workers.sh" ]]; then
|
as_root() {
|
||||||
if [[ -z "${CODEBERG_TOKEN:-}" ]]; then
|
if [[ "$(id -u)" = "0" ]]; then
|
||||||
echo "error: workspace missing repo files and CODEBERG_TOKEN is not set"
|
"$@"
|
||||||
|
elif command -v sudo >/dev/null 2>&1; then
|
||||||
|
sudo "$@"
|
||||||
|
else
|
||||||
|
echo "error: need root or sudo to install runner dependencies"
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
git clone "https://every-channel:${CODEBERG_TOKEN}@codeberg.org/every-channel/every.channel.git" .repo
|
}
|
||||||
cd .repo
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
as_root apt-get update
|
||||||
|
as_root apt-get install -y curl ca-certificates nodejs npm age
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
as_root apk add --no-cache curl ca-certificates nodejs npm age
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Configure CI Age identity
|
||||||
|
env:
|
||||||
|
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -z "${AGE_FORGE_SSH_KEY:-}" ]]; then
|
||||||
|
echo "error: missing Actions secret AGE_FORGE_SSH_KEY"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
install -d -m 700 "$HOME/.ssh"
|
||||||
|
if [[ "${AGE_FORGE_SSH_KEY}" == "-----BEGIN OPENSSH PRIVATE KEY-----"* ]]; then
|
||||||
|
printf '%s\n' "${AGE_FORGE_SSH_KEY}" > "$HOME/.ssh/age_forge_ed25519"
|
||||||
|
else
|
||||||
|
printf '%s' "${AGE_FORGE_SSH_KEY}" | base64 -d > "$HOME/.ssh/age_forge_ed25519"
|
||||||
|
fi
|
||||||
|
chmod 600 "$HOME/.ssh/age_forge_ed25519"
|
||||||
|
|
||||||
|
- name: Decrypt CI secrets from repo
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
key_file="$HOME/.ssh/age_forge_ed25519"
|
||||||
|
secret_file="secrets/cloudflare-api-token.age"
|
||||||
|
if [[ ! -f "$secret_file" ]]; then
|
||||||
|
echo "error: missing ${secret_file}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
CLOUDFLARE_API_TOKEN="$(age -d -i "$key_file" "$secret_file")"
|
||||||
|
if [[ -z "${CLOUDFLARE_API_TOKEN}" ]]; then
|
||||||
|
echo "error: decrypted CLOUDFLARE_API_TOKEN is empty"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
echo "::add-mask::${CLOUDFLARE_API_TOKEN}"
|
||||||
|
echo "CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Build site (Dioxus web)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
if ! command -v cargo >/dev/null 2>&1; then
|
if ! command -v cargo >/dev/null 2>&1; then
|
||||||
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal
|
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||||
. "$HOME/.cargo/env"
|
. "$HOME/.cargo/env"
|
||||||
|
|
@ -41,14 +92,13 @@ jobs:
|
||||||
cargo install trunk --locked
|
cargo install trunk --locked
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v npm >/dev/null 2>&1; then
|
|
||||||
echo "error: npm is not available on this runner"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd apps/tauri/ui
|
cd apps/tauri/ui
|
||||||
trunk build --release --public-url /
|
trunk build --release --public-url /
|
||||||
|
|
||||||
|
- name: Deploy worker
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
cd ../../../deploy/cloudflare-worker
|
cd ../../../deploy/cloudflare-worker
|
||||||
npm ci
|
npm ci
|
||||||
npx wrangler deploy
|
npx wrangler deploy
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Status: Draft
|
Status: Draft
|
||||||
|
|
||||||
|
Note: CI handling in this proposal is superseded by `ECP-0062` (single SSH identity + repo-encrypted secrets for deploy).
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Provide a simple, repo-native way to manage a small set of long-lived tokens for local development without committing plaintext secrets:
|
Provide a simple, repo-native way to manage a small set of long-lived tokens for local development without committing plaintext secrets:
|
||||||
|
|
|
||||||
42
evolution/proposals/ECP-0062-ci-age-key-secrets.md
Normal file
42
evolution/proposals/ECP-0062-ci-age-key-secrets.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# ECP-0062: CI Secrets via Single SSH Identity + Repo-Encrypted Age Files
|
||||||
|
|
||||||
|
Status: Draft
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Keep CI secret handling minimal and auditable:
|
||||||
|
|
||||||
|
- one Forgejo Actions secret containing an SSH private key (`AGE_FORGE_SSH_KEY`),
|
||||||
|
- all runtime credentials stored in git as encrypted `.age` files,
|
||||||
|
- no CI dependence on repo cloning tokens (`CODEBERG_TOKEN`) for deploy.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Replacing local developer token helpers (`scripts/fj-auth-codeberg.sh`).
|
||||||
|
- Defining protocol-level stream key distribution.
|
||||||
|
|
||||||
|
## Proposal
|
||||||
|
|
||||||
|
1. Deploy workflow uses `actions/checkout` with `github.token` and drops the clone fallback path.
|
||||||
|
2. Deploy workflow requires one secret only: `AGE_FORGE_SSH_KEY`.
|
||||||
|
3. Deploy workflow decrypts `secrets/cloudflare-api-token.age` at runtime via `age -d -i <key>`.
|
||||||
|
4. `CLOUDFLARE_API_TOKEN` is exported into `GITHUB_ENV` only for the current job.
|
||||||
|
5. `CODEBERG_TOKEN` is removed from deploy workflow requirements.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
This matches the key.store operational model:
|
||||||
|
|
||||||
|
- one root automation identity in Forgejo,
|
||||||
|
- encrypted secrets versioned in-repo,
|
||||||
|
- no plaintext token files in CI configuration.
|
||||||
|
|
||||||
|
It reduces secret sprawl, removes accidental token coupling, and keeps deploy bootstrap deterministic.
|
||||||
|
|
||||||
|
## Rollout / Reversibility
|
||||||
|
|
||||||
|
- Additive migration:
|
||||||
|
- set `AGE_FORGE_SSH_KEY` in Forgejo,
|
||||||
|
- commit encrypted `secrets/cloudflare-api-token.age`,
|
||||||
|
- run deploy.
|
||||||
|
- Reversible by reintroducing direct Actions secret env injection if needed.
|
||||||
|
|
@ -37,6 +37,11 @@
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
packages = {
|
||||||
|
agenix = agenixPkg;
|
||||||
|
fj = pkgs.forgejo-cli;
|
||||||
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
rust
|
rust
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,19 @@ cd "${root}"
|
||||||
# Auth token source order:
|
# Auth token source order:
|
||||||
# 1) CODEBERG_TOKEN env var
|
# 1) CODEBERG_TOKEN env var
|
||||||
# 2) `agenix -d secrets/codeberg-token.age` (optional)
|
# 2) `agenix -d secrets/codeberg-token.age` (optional)
|
||||||
|
# 3) `age -d -i <identity> secrets/codeberg-token.age` (optional)
|
||||||
|
|
||||||
rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}"
|
rules_file="${EVERY_CHANNEL_AGE_RULES_FILE:-./secrets.nix}"
|
||||||
identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
identity_file="${EVERY_CHANNEL_AGE_IDENTITY_FILE:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
||||||
|
|
||||||
if [[ -z "${CODEBERG_TOKEN:-}" && -f secrets/codeberg-token.age && -x "$(command -v agenix)" ]]; then
|
if [[ -z "${CODEBERG_TOKEN:-}" && -f secrets/codeberg-token.age ]]; then
|
||||||
export CODEBERG_TOKEN
|
if command -v agenix >/dev/null 2>&1; then
|
||||||
CODEBERG_TOKEN="$(RULES="${rules_file}" agenix -d secrets/codeberg-token.age -i "${identity_file}")"
|
export CODEBERG_TOKEN
|
||||||
|
CODEBERG_TOKEN="$(RULES="${rules_file}" agenix -d secrets/codeberg-token.age -i "${identity_file}")"
|
||||||
|
elif command -v age >/dev/null 2>&1; then
|
||||||
|
export CODEBERG_TOKEN
|
||||||
|
CODEBERG_TOKEN="$(age -d -i "${identity_file}" secrets/codeberg-token.age)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "${CODEBERG_TOKEN:-}" ]]; then
|
if [[ -z "${CODEBERG_TOKEN:-}" ]]; then
|
||||||
|
|
|
||||||
33
scripts/fj-set-age-key-secret.sh
Executable file
33
scripts/fj-set-age-key-secret.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "${root}"
|
||||||
|
|
||||||
|
host="${EVERY_CHANNEL_FORGE_HOST:-https://codeberg.org}"
|
||||||
|
repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}"
|
||||||
|
secret_name="${EVERY_CHANNEL_FORGE_AGE_SECRET_NAME:-AGE_FORGE_SSH_KEY}"
|
||||||
|
key_path="${1:-$HOME/.config/every.channel/keys/founder_ed25519}"
|
||||||
|
|
||||||
|
if [[ ! -f "${key_path}" ]]; then
|
||||||
|
echo "error: key file not found: ${key_path}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! command -v fj >/dev/null 2>&1; then
|
||||||
|
echo "error: fj not found in PATH (run: nix develop)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${root}/scripts/fj-auth-codeberg.sh" >/dev/null
|
||||||
|
|
||||||
|
key_data="$(base64 < "${key_path}" | tr -d '\n')"
|
||||||
|
if [[ -z "${key_data}" ]]; then
|
||||||
|
echo "error: key file is empty: ${key_path}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upsert by delete/create because fj currently exposes create/delete.
|
||||||
|
fj -H "${host}" actions -r "${repo}" secrets delete "${secret_name}" >/dev/null 2>&1 || true
|
||||||
|
fj -H "${host}" actions -r "${repo}" secrets create "${secret_name}" "${key_data}" >/dev/null
|
||||||
|
|
||||||
|
echo "ok: set ${secret_name} on ${repo} via ${host} (base64-encoded)"
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
let
|
let
|
||||||
# Founder SSH public key (recipient). Safe to commit.
|
# Founder SSH public key (recipient). Safe to commit.
|
||||||
founder = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJCBTSEEcBOhOkf3WF1e8xmblAZHvgTibFsqck2GY8D/";
|
founder = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJCBTSEEcBOhOkf3WF1e8xmblAZHvgTibFsqck2GY8D/";
|
||||||
|
# Forge automation SSH public key (recipient). Safe to commit.
|
||||||
|
forge = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFmKJt5+uilix5Ldiaaq1BhrYNjmV5lHcW7D/5inCCnO forge@every.channel";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
"secrets/cloudflare-api-token.age".publicKeys = [ founder ];
|
"secrets/cloudflare-api-token.age".publicKeys = [ founder forge ];
|
||||||
"secrets/codeberg-token.age".publicKeys = [ founder ];
|
"secrets/codeberg-token.age".publicKeys = [ founder forge ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
# Secrets (agenix)
|
# Secrets (agenix)
|
||||||
|
|
||||||
This repo supports optional local secrets management via `agenix`.
|
This repo supports local + CI secrets management via `agenix`/`age`.
|
||||||
|
|
||||||
CI should prefer Forgejo Actions secrets (e.g. `CLOUDFLARE_API_TOKEN`) rather than decrypting secrets in runners.
|
CI deploys use one Forgejo Actions secret:
|
||||||
|
|
||||||
|
- `AGE_FORGE_SSH_KEY`: SSH private key used to decrypt repo-tracked `.age` files.
|
||||||
|
|
||||||
|
Set/update it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix develop -c ./scripts/fj-set-age-key-secret.sh ~/.config/every.channel/keys/forge_ci_ed25519
|
||||||
|
```
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `secrets/secrets.nix`: recipients + secret file mapping
|
- `secrets/secrets.nix`: recipients + secret file mapping
|
||||||
- `secrets/cloudflare-api-token.age`: encrypted Cloudflare API token (optional)
|
- `secrets/cloudflare-api-token.age`: encrypted Cloudflare API token (used by deploy workflow)
|
||||||
- `secrets/codeberg-token.age`: encrypted Codeberg/Forgejo token for `fj` (optional)
|
- `secrets/codeberg-token.age`: encrypted Codeberg/Forgejo token for `fj` (optional)
|
||||||
|
|
||||||
## Create / edit secrets (local)
|
## Create / edit secrets (local)
|
||||||
|
|
@ -32,4 +40,4 @@ agenix -d secrets/cloudflare-api-token.age
|
||||||
|
|
||||||
## Decryption identity
|
## Decryption identity
|
||||||
|
|
||||||
`agenix` decrypts using your local SSH key material. The private key must be available locally but is never committed to the repo.
|
`agenix`/`age` decrypts using SSH private key material. The private key must be available locally (or injected as CI secret) and is never committed to the repo.
|
||||||
|
|
|
||||||
7
secrets/cloudflare-api-token.age
Normal file
7
secrets/cloudflare-api-token.age
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
age-encryption.org/v1
|
||||||
|
-> ssh-ed25519 29OJ4A zgIVFl3ybukZblX6BIQwL+safny154q0FzRS4KTXV3w
|
||||||
|
4u7A8ymx0dZQE7oKnzdzWtObT+BZV1HtPiWDHW9WGWA
|
||||||
|
-> ssh-ed25519 E6Q+Lw WwMbUn454lqkAZtt9GOGWZAl4dvZ2YEQatK3rViz6DM
|
||||||
|
VD3c+PxIwZ9cZmv7U2bXFiN1UlTQYImbeap1v2MvnBw
|
||||||
|
--- gH+1U1LQN7CB7L2Tk7oLwgWjnqfFTNVau3NJSJUAEJ8
|
||||||
|
,™)ò,ÍMMø!ËçééÈ4½Þ¯i žgÄ<67>GÙj—⛽ËR¡§Ø:©¶vj€oÕ¯tlåʬZÀÜ)Öï•N”Éju<6A>‚Û©
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
age-encryption.org/v1
|
age-encryption.org/v1
|
||||||
-> ssh-ed25519 29OJ4A G6byj6PhWofxSh8K5FGSqBs5W5uKtyJ2MGY1JFb+STc
|
-> ssh-ed25519 29OJ4A 0tKPVpGaDJTMMhyy9/+rswkNttrVhttZFM6ORiMapjk
|
||||||
d25eWVNmz2+0zKVVRZ/Pib4YZClhJrML6s3hbLh9rMU
|
/EVUgqbde8gJQubCzOeTfVIeE1VYF4W681LxfA1fp0k
|
||||||
--- t/6aoMSRLI8vay71VugNOGwKHjHteiC+SinD6gQARYM
|
-> ssh-ed25519 E6Q+Lw Wn4IY+Flb2mpZFWg/iMGSua298EEiBJgMqdnv+BCkA4
|
||||||
¹ˆ8ùEÉâò /‚³?ᢿwÝØJlSñæäíz<C3AD>i–8Ç)†äÿƒ`ã;Bæ4LåÑT„Àö?£;ãÀzÇšÐé\
|
hZBMEYsq9GCLLAh6KXaJbNSs3sks/oH74hoQUPDvMKM
|
||||||
|
--- m5RYGCTBkYSET5SfeqdOhfLij9sPDzglIPwVnmaEeGA
|
||||||
|
c$ÚýŸ2tÒ<74>¡0ºàé3êïßU+u žõ7kU<6B>o")оÀ¦Z¢BiÀÉÃö‰TA*ßy‰LÖ|û=ñlÑšÌî/BîP
|
||||||
Loading…
Add table
Add a link
Reference in a new issue