diff --git a/.forgejo/Dockerfile b/.forgejo/Dockerfile index 73d5916..fed065b 100644 --- a/.forgejo/Dockerfile +++ b/.forgejo/Dockerfile @@ -10,6 +10,7 @@ FROM ghcr.io/catthehacker/ubuntu:go-24.04 RUN apt-get update && apt-get install -y --no-install-recommends \ stunnel4 \ netcat-openbsd \ + age \ && rm -rf /var/lib/apt/lists/* # Dagger CLI — pinned to match the engine version on the runner host diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index a7887b0..418db7a 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -65,6 +65,7 @@ jobs: run: | command -v dagger >/dev/null 2>&1 || { echo "ERROR: dagger is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } command -v task >/dev/null 2>&1 || { echo "ERROR: task is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } + command -v age >/dev/null 2>&1 || { echo "ERROR: age is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } dpkg -s stunnel4 netcat-openbsd >/dev/null 2>&1 || { echo "ERROR: stunnel4/netcat-openbsd are not installed in the runner image. Add them to .forgejo/Dockerfile."; exit 1; } - name: Setup Dagger Remote Engine (via stunnel) @@ -75,11 +76,15 @@ jobs: DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }} run: scripts/setup_dagger_remote.sh - - name: Run Android Tests on Firebase Test Lab - if: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY != '' }} + - name: Decrypt production secrets + if: ${{ secrets.SECRETS_AGE_KEY != '' }} + env: + SECRETS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY }} + run: scripts/secrets-decrypt.sh + + - name: Run Android Tests on Firebase Test Lab + if: env.FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY != '' env: - FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY: ${{ secrets.FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY }} - FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }} DAGGER_NO_NAG: "1" run: task test-android-firebase @@ -103,6 +108,7 @@ jobs: run: | command -v dagger >/dev/null 2>&1 || { echo "ERROR: dagger is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } command -v task >/dev/null 2>&1 || { echo "ERROR: task is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } + command -v age >/dev/null 2>&1 || { echo "ERROR: age is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } dpkg -s stunnel4 netcat-openbsd >/dev/null 2>&1 || { echo "ERROR: stunnel4/netcat-openbsd are not installed in the runner image. Add them to .forgejo/Dockerfile."; exit 1; } - name: Setup Dagger Remote Engine (via stunnel) @@ -113,12 +119,15 @@ jobs: DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }} run: scripts/setup_dagger_remote.sh - - name: Publish Android to Play Store - if: ${{ secrets.PLAY_STORE_CONFIG_JSON != '' }} + - name: Decrypt production secrets + if: ${{ secrets.SECRETS_AGE_KEY != '' }} + env: + SECRETS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY }} + run: scripts/secrets-decrypt.sh + + - name: Publish Android to Play Store + if: env.PLAY_STORE_CONFIG_JSON != '' env: - ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_STORE_CONFIG_JSON }} DAGGER_NO_NAG: "1" run: task publish-android @@ -142,6 +151,7 @@ jobs: run: | command -v dagger >/dev/null 2>&1 || { echo "ERROR: dagger is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } command -v task >/dev/null 2>&1 || { echo "ERROR: task is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } + command -v age >/dev/null 2>&1 || { echo "ERROR: age is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } dpkg -s stunnel4 netcat-openbsd >/dev/null 2>&1 || { echo "ERROR: stunnel4/netcat-openbsd are not installed in the runner image. Add them to .forgejo/Dockerfile."; exit 1; } - name: Setup Dagger Remote Engine (via stunnel) @@ -152,15 +162,15 @@ jobs: DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }} run: scripts/setup_dagger_remote.sh - - name: Build & Deploy APK to server - if: ${{ secrets.SSH_PRIVATE_KEY != '' }} + - name: Decrypt production secrets + if: ${{ secrets.SECRETS_AGE_KEY != '' }} + env: + SECRETS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY }} + run: scripts/secrets-decrypt.sh + + - name: Build & Deploy APK to server + if: env.SSH_PRIVATE_KEY != '' env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} - SSH_USER: ${{ secrets.SSH_USER }} - SSH_HOST: ${{ secrets.SSH_HOST }} - ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} DAGGER_NO_NAG: "1" run: task deploy-apk @@ -184,6 +194,7 @@ jobs: run: | command -v dagger >/dev/null 2>&1 || { echo "ERROR: dagger is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } command -v task >/dev/null 2>&1 || { echo "ERROR: task is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } + command -v age >/dev/null 2>&1 || { echo "ERROR: age is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } dpkg -s stunnel4 netcat-openbsd >/dev/null 2>&1 || { echo "ERROR: stunnel4/netcat-openbsd are not installed in the runner image. Add them to .forgejo/Dockerfile."; exit 1; } - name: Setup Dagger Remote Engine (via stunnel) @@ -194,13 +205,15 @@ jobs: DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }} run: scripts/setup_dagger_remote.sh - - name: Build & Deploy Linux to server - if: ${{ secrets.SSH_PRIVATE_KEY != '' }} + - name: Decrypt production secrets + if: ${{ secrets.SECRETS_AGE_KEY != '' }} + env: + SECRETS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY }} + run: scripts/secrets-decrypt.sh + + - name: Build & Deploy Linux to server + if: env.SSH_PRIVATE_KEY != '' env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} - SSH_USER: ${{ secrets.SSH_USER }} - SSH_HOST: ${{ secrets.SSH_HOST }} DAGGER_NO_NAG: "1" run: task deploy-linux @@ -226,6 +239,7 @@ jobs: run: | command -v dagger >/dev/null 2>&1 || { echo "ERROR: dagger is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } command -v task >/dev/null 2>&1 || { echo "ERROR: task is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } + command -v age >/dev/null 2>&1 || { echo "ERROR: age is not installed in the runner image. Add it to .forgejo/Dockerfile."; exit 1; } dpkg -s stunnel4 netcat-openbsd >/dev/null 2>&1 || { echo "ERROR: stunnel4/netcat-openbsd are not installed in the runner image. Add them to .forgejo/Dockerfile."; exit 1; } - name: Setup Dagger Remote Engine (via stunnel) @@ -236,13 +250,15 @@ jobs: DAGGER_CLIENT_KEY: ${{ secrets.DAGGER_CLIENT_KEY }} run: scripts/setup_dagger_remote.sh - - name: Generate build history and deploy website - if: ${{ secrets.SSH_PRIVATE_KEY != '' }} + - name: Decrypt production secrets + if: ${{ secrets.SECRETS_AGE_KEY != '' }} + env: + SECRETS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY }} + run: scripts/secrets-decrypt.sh + + - name: Generate build history and deploy website + if: env.SSH_PRIVATE_KEY != '' env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} - SSH_USER: ${{ secrets.SSH_USER }} - SSH_HOST: ${{ secrets.SSH_HOST }} DAGGER_NO_NAG: "1" run: task publish-website diff --git a/.gitignore b/.gitignore index de47e6c..7608eac 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ assets/changelog.txt .env.local .envrc .direnv/ +secrets.env # plaintext secrets — encrypted version (secrets.age) is committed # --- Android --- android/.gradle/ diff --git a/DAGGER.md b/DAGGER.md index 5f7f3de..e17cea1 100644 --- a/DAGGER.md +++ b/DAGGER.md @@ -174,10 +174,70 @@ Run a secret manager co-located with the Dagger host. The CI job authenticates w - Vault itself becomes a security-critical single point of failure. - Operational overhead likely disproportionate for a small single-developer project. +### Option 5: Encrypted secrets file (age) — **implemented** + +Store all production secrets in a file (`secrets.env`) that is encrypted with +[age](https://age-encryption.org/) into `secrets.age`. The encrypted file is +committed to the repository. Only the age private key — a single string — is +stored in Codeberg as `SECRETS_AGE_KEY`. Any CI job or developer with the key +can decrypt the file and obtain all secrets. + +**How it works:** + +1. Generate a key pair once: + ```bash + age-keygen -o ~/.config/age/sharedinbox.key + age-keygen -y ~/.config/age/sharedinbox.key > .age-public-key + ``` +2. Copy `secrets.env.example` to `secrets.env`, fill in all values, then encrypt: + ```bash + scripts/secrets-encrypt.sh # reads public key from .age-public-key + git add secrets.age && git commit -m "chore: update encrypted secrets" + ``` +3. Add the private key content as `SECRETS_AGE_KEY` in Codeberg repository secrets. +4. CI jobs call `scripts/secrets-decrypt.sh` (with `SECRETS_AGE_KEY` set) before + any step that needs production credentials. The script writes each variable + to `$GITHUB_ENV` so subsequent steps see them automatically. + +**Keeping local and CI in sync:** +When you rotate a secret locally, update `secrets.env`, re-run +`scripts/secrets-encrypt.sh`, and commit the new `secrets.age`. CI will pick +up the fresh secrets on the next push — no manual CI variable updates needed. + +Multi-line values (SSH keys, certificates) must be stored as a single line +with `\n` escape sequences inside double quotes. Example: +``` +SSH_PRIVATE_KEY="
\n\n