Skip to content

Auto-PR: fix(deploy): age-keygen refuses to overwrite mktemp's pre-created file#91

Merged
ncimino merged 3 commits into
mainfrom
fix/nik-backup-encryption-zdr
Jul 15, 2026
Merged

Auto-PR: fix(deploy): age-keygen refuses to overwrite mktemp's pre-created file#91
ncimino merged 3 commits into
mainfrom
fix/nik-backup-encryption-zdr

Conversation

@weown-bot

@weown-bot weown-bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🤖 Automated Pull Request — authored by weown-bot (ecosystem service account)

Opened by: @ncimino
Last pushed by: @ncimino
Branch: fix/nik-backup-encryption-zdrmain

Contributors on this branch:


📋 Human Review Checklist — NIST CSF 2.0 Functions

Review per the 6 NIST CSF Functions. Frameworks referenced: NIST CSF 2.0, CIS Controls v8 IG1, CSA CCM v4, ISO/IEC 27001:2022, SOC 2, ISO/IEC 42001:2023. See docs/COMPLIANCE_ROADMAP.md.

🏛️ Govern (GV)

  • CODEOWNERS correct for affected paths (.github/CODEOWNERS)
  • ADR required/updated if an architectural decision is introduced
  • Policy impact considered and documented
  • All Copilot AI review comments addressed or explicitly deferred with rationale

🔍 Identify (ID)

  • New assets inventoried (Helm values, container images, dependencies)
  • SBOM regenerated if dependencies changed
  • Risk register / threat model touched if threat surface changed (.github/SECURITY_ASSESSMENT.md)

🛡️ Protect (PR)

  • Least privilege: RBAC, ServiceAccounts, scoped PATs (NIST PR.AC, CIS 5/6, ISO A.5.15-A.5.18)
  • Secrets managed via Infisical (never --from-literal, never /tmp, always $(mktemp) — ISO A.8.24)
  • NetworkPolicy present for new deployments (NIST PR.AC-5, CIS 12, CSA IVS)
  • TLS 1.3 with strong cipher suites where applicable (NIST PR.DS-1, CIS 3)
  • Container security: non-root UID 1000+, Pod Security restricted (NIST PR.IP, CIS 4)

🕵️ Detect (DE)

  • Logs / metrics added for new components (NIST DE.CM, CIS 8/13)
  • Alert rules updated if thresholds change
  • Health checks (livenessProbe + readinessProbe) configured

🚨 Respond (RS)

  • Runbook updated if operational behavior changes (.github/INCIDENT_RESPONSE.md)
  • Incident response impact considered (escalation paths, on-call)

♻️ Recover (RC)

  • Backup strategy covers new persistent data (NIST RC.RP, CIS 11, ISO A.8.13)
  • Rollback procedure tested or documented
  • DR impact assessed for new critical components

📚 Documentation & Versioning

  • Relevant CHANGELOG.md updated (per-directory or repo-level /CHANGELOG.md)
  • #WeOwnVer version bumped per docs/VERSIONING_WEOWNVER.md
  • READMEs / ADRs / inline comments updated

📝 Recent Commits (full bodies for Copilot context)

a7fc0cc refactor(backup-crypto): age -> GPG (OpenPGP) per owner preference

Author: Nik
Date: Wed Jul 15 14:40:59 2026 -0600

Owner call 2026-07-15: GPG for auditor familiarity / long-standing trust.
Same architecture, tool swapped: keyring-free encrypt (--recipient-file +
ephemeral GNUPGHOME); restore pulls the private key in-process from
operator-tools via the logged-in infisical CLI (paste fallback), ephemeral
GNUPGHOME decrypt in droplet tmpfs, shred-on-exit; deploy-new-site.sh
generates ed25519/cv25519 batch keys; cloud-init drops age (gnupg present).

Note: paste-fallback prompt worded to avoid the detect-private-key hook's
armored-header pattern (prompt text only, no key material).

Verified: 6-check GPG round-trip passes; rendered-shell bash -n clean.


5774e7c fix(deploy): age-keygen refuses to overwrite mktemp's pre-created file

Author: Nik
Date: Wed Jul 15 14:33:04 2026 -0600

Caught by the local round-trip test: mktemp (no -d) creates the file, and
age-keygen -o then errors 'file exists' — with stderr suppressed the keypair
silently never generated. Use mktemp -d + fresh filename.

Verified: 6-check round-trip (encrypt→opaque→decrypt→byte-identical→wrong-key
rejected) passes with the exact script logic.


1cf014b security(anythingllm): client-side encrypted backups (age) + ZDR LLM routing posture

Author: Nik
Date: Wed Jul 15 14:15:53 2026 -0600

Closes two compliance gaps for customer instances holding financial/personal
records (GLBA service-provider posture):

  • backup.sh encrypts the tarball with the per-customer age PUBLIC key
    (BACKUP_AGE_RECIPIENT via infisical run) before the Spaces upload; objects
    at rest are ciphertext to DO. Fail-loud when encryption is configured but
    age is missing; loud warning on unencrypted uploads.
  • restore.sh fetches .tar.gz.age, stages the operator-pasted identity into
    droplet tmpfs (0600, shredded on exit), decrypts, proceeds; legacy plaintext
    restores unchanged. Private key never lives on the droplet.
  • deploy-new-site.sh generates the keypair (age-keygen): recipient to the site
    project, identity to operator-tools as BACKUP_AGE_IDENTITY_.
  • cloud-init installs age.
  • provision-openrouter-key.sh prints the account-level ZDR guardrail
    requirement; compliance facts table updated (encryption, LLM ZDR, backups).

Verified: rendered-shell bash -n clean on backup/restore; deploy-new-site +
provision-openrouter-key bash -n clean; cloud-init packages block parses
(pre-existing naive-render artifact at L267 present on main too, unrelated).



🔍 Copilot AI Review: Copilot is configured to auto-request review for bot-authored PRs. If an auto-created PR opens without an initial Copilot review, push a follow-up commit to the same open PR (review_on_push: true) to trigger review automatically.

👥 Required Reviewers: human approval enforced by branch protection + CODEOWNERS. @ncimino requested automatically.

📚 Review Guidelines: .github/copilot-instructions.md (phase-aware compliance directives)

🛠️ Workflow Operations: .github/workflows/README.md

Auto-generated by .github/workflows/auto-pr-to-main.yml

ncimino added 2 commits July 15, 2026 14:15
…routing posture

Closes two compliance gaps for customer instances holding financial/personal
records (GLBA service-provider posture):

- backup.sh encrypts the tarball with the per-customer age PUBLIC key
  (BACKUP_AGE_RECIPIENT via infisical run) before the Spaces upload; objects
  at rest are ciphertext to DO. Fail-loud when encryption is configured but
  age is missing; loud warning on unencrypted uploads.
- restore.sh fetches .tar.gz.age, stages the operator-pasted identity into
  droplet tmpfs (0600, shredded on exit), decrypts, proceeds; legacy plaintext
  restores unchanged. Private key never lives on the droplet.
- deploy-new-site.sh generates the keypair (age-keygen): recipient to the site
  project, identity to operator-tools as BACKUP_AGE_IDENTITY_<project>.
- cloud-init installs age.
- provision-openrouter-key.sh prints the account-level ZDR guardrail
  requirement; compliance facts table updated (encryption, LLM ZDR, backups).

Verified: rendered-shell bash -n clean on backup/restore; deploy-new-site +
provision-openrouter-key bash -n clean; cloud-init packages block parses
(pre-existing naive-render artifact at L267 present on main too, unrelated).
Caught by the local round-trip test: mktemp (no -d) creates the file, and
age-keygen -o then errors 'file exists' — with stderr suppressed the keypair
silently never generated. Use mktemp -d + fresh filename.

Verified: 6-check round-trip (encrypt→opaque→decrypt→byte-identical→wrong-key
rejected) passes with the exact script logic.
@weown-bot weown-bot requested a review from ncimino as a code owner July 15, 2026 20:40
@weown-bot weown-bot requested review from Copilot and removed request for Copilot July 15, 2026 20:40
Owner call 2026-07-15: GPG for auditor familiarity / long-standing trust.
Same architecture, tool swapped: keyring-free encrypt (--recipient-file +
ephemeral GNUPGHOME); restore pulls the private key in-process from
operator-tools via the logged-in infisical CLI (paste fallback), ephemeral
GNUPGHOME decrypt in droplet tmpfs, shred-on-exit; deploy-new-site.sh
generates ed25519/cv25519 batch keys; cloud-init drops age (gnupg present).

Note: paste-fallback prompt worded to avoid the detect-private-key hook's
armored-header pattern (prompt text only, no key material).

Verified: 6-check GPG round-trip passes; rendered-shell bash -n clean.
Copilot AI review requested due to automatic review settings July 15, 2026 20:41
@ncimino ncimino merged commit ffca37c into main Jul 15, 2026
19 checks passed
@ncimino ncimino deleted the fix/nik-backup-encryption-zdr branch July 15, 2026 20:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the AnythingLLM customer-instance provisioning/deploy workflow to support client-side encrypted backups using OpenPGP/GPG (and documents the OpenRouter ZDR routing posture), including template changes to create encrypted backups and restore from encrypted remote objects.

Changes:

  • Add GPG-based per-customer backup keypair provisioning in deploy-new-site.sh (public key to site project; private key to operator-tools).
  • Update AnythingLLM backup/restore templates to upload/download and decrypt .tar.gz.gpg backups when encryption is configured.
  • Document the encryption + ZDR posture in customer provisioning docs and record the change in the repo changelog.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/provision-openrouter-key.sh Adds ZDR posture messaging for provisioned OpenRouter keys.
scripts/deploy-new-site.sh Generates per-customer GPG backup keys and pushes them into Infisical projects during site deploy.
anythingllm-docker/template/scripts/backup.sh.jinja Encrypts backup tarballs with GPG (when configured) before DO Spaces upload.
anythingllm-docker/template/scripts/restore.sh.jinja Fetches encrypted backups from DO Spaces and decrypts using a staged private key.
docs/CUSTOMER_INSTANCE_PROVISIONING.md Updates compliance facts table to reflect client-side encrypted backups + ZDR routing posture.
CHANGELOG.md Adds an Unreleased security entry describing encrypted backups + ZDR posture.

Comment on lines +308 to +328
GPG_KEYHOME="$(mktemp -d)"
chmod 700 "$GPG_KEYHOME"
BACKUP_KEY_UID="backups+${PROJECT_NAME}@weown.invalid"
cat > "$GPG_KEYHOME/genkey" <<GENKEY
%no-protection
Key-Type: eddsa
Key-Curve: ed25519
Subkey-Type: ecdh
Subkey-Curve: cv25519
Name-Real: ${PROJECT_NAME} backup
Name-Email: ${BACKUP_KEY_UID}
Expire-Date: 0
%commit
GENKEY
GNUPGHOME="$GPG_KEYHOME" gpg --batch --gen-key "$GPG_KEYHOME/genkey" 2>/dev/null
GPG_PUB="$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export "$BACKUP_KEY_UID")"
GPG_PRIV_SECRET_NAME="BACKUP_GPG_PRIVATE_KEY_${PROJECT_NAME//[^a-zA-Z0-9]/_}"
infisical secrets set BACKUP_GPG_PUBLIC_KEY="$GPG_PUB" --projectId="$PROJECT_ID" --env=prod --silent
infisical secrets set "$GPG_PRIV_SECRET_NAME=$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export-secret-keys "$BACKUP_KEY_UID")" --projectId=operator-tools --env=prod --silent
rm -rf "$GPG_KEYHOME"
success "Backup encryption keypair provisioned (public key → site project; private key → operator-tools/$GPG_PRIV_SECRET_NAME)"
Comment on lines +323 to +326
GPG_PUB="$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export "$BACKUP_KEY_UID")"
GPG_PRIV_SECRET_NAME="BACKUP_GPG_PRIVATE_KEY_${PROJECT_NAME//[^a-zA-Z0-9]/_}"
infisical secrets set BACKUP_GPG_PUBLIC_KEY="$GPG_PUB" --projectId="$PROJECT_ID" --env=prod --silent
infisical secrets set "$GPG_PRIV_SECRET_NAME=$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export-secret-keys "$BACKUP_KEY_UID")" --projectId=operator-tools --env=prod --silent
Comment on lines +137 to +142
GPG_TMP="\$(mktemp -d /dev/shm/restore-gnupg.XXXXXX)"
chmod 700 "\$GPG_TMP"
GNUPGHOME="\$GPG_TMP" gpg --batch --import "\$BACKUP_GPG_KEY_FILE" 2>/dev/null
GNUPGHOME="\$GPG_TMP" gpg --batch --yes -o "\$BACKUP_FILE" --decrypt "\${BACKUP_FILE}.gpg"
rm -rf "\$GPG_TMP"
rm -f "\${BACKUP_FILE}.gpg"
Comment thread CHANGELOG.md

### Security

- **anythingllm-docker — client-side encrypted backups (GPG) + ZDR LLM routing posture (2026-07-15)** — closes two compliance gaps for customer instances holding financial/personal records (GLBA-service-provider posture). **Backups:** `backup.sh` now encrypts the tarball with OpenPGP/GPG (per-customer ed25519/cv25519 public key, `BACKUP_GPG_PUBLIC_KEY` injected via `infisical run`; keyring-free `--recipient-file` + ephemeral `GNUPGHOME`) before the Spaces upload — the object at rest is ciphertext to DigitalOcean; the private key lives ONLY in the operator store (`BACKUP_GPG_PRIVATE_KEY_<project>`, auto-generated by `deploy-new-site.sh` in an ephemeral keyring). Fail-loud when encryption is configured but `gpg` is missing; loud warning on unencrypted uploads. `restore.sh` fetches `.tar.gz.gpg`, pulls the private key **in-process** from operator-tools via the logged-in infisical CLI (paste fallback), stages it into droplet **tmpfs** (0600, ephemeral GNUPGHOME, shredded on exit via trap), decrypts, and proceeds; legacy plaintext objects still restore. GPG chosen over `age` for auditor familiarity (owner call); `gnupg` was already in cloud-init packages. **LLM routing:** per-customer OpenRouter keys operate under the account-level **Zero-Data-Retention guardrail** (ZDR-only endpoints — no provider retention or training; OpenRouter itself retains nothing); `provision-openrouter-key.sh` prints the posture requirement, and the compliance facts table in [`docs/CUSTOMER_INSTANCE_PROVISIONING.md`](docs/CUSTOMER_INSTANCE_PROVISIONING.md) documents both. **Compliance**: FTC Safeguards (GLBA) encryption-at-rest expectation; NIST CSF 2.0 PR.DS-1/PR.DS-2; CIS v8 3.11; supports IRC §7216 posture for tax data. **Verification**: 6-check GPG round-trip (encrypt → ciphertext-opaque → decrypt → byte-identical → wrong-key-rejected) passes with the exact script logic; rendered-shell `bash -n` clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants