fix(manage-server): encrypt pre-upgrade pg_dump backup on all platforms (#1119) #30

Merged
amiryahaya merged 1 commit from fix/1119-windows-backup-encryption into main 2026-09-23 14:29:25 +02:00
Owner

Closes triton#1119, plus a second gap found while scoping it.

1. Sync gap (bash side, not previously tracked)

triton repo's PR #1117 (issue #1114) added scripts/backup-encrypt.sh
and wired BACKUP_ENCRYPTION_KEY into
scripts/deploy/manage-server/{install.sh,upgrade.sh,env.template} on
main, but per that repo's CLAUDE.md "copy to triton-install and push
both repos" rule, this was never actually copied here. Confirmed
by diff: this repo's manage-server/install.sh/upgrade.sh had zero
mention of BACKUP_ENCRYPTION_KEY, and backup-encrypt.sh didn't
exist here at all. Real customers installing via curl | bash today
got fully unencrypted pre-upgrade backups despite #1114 supposedly
fixing this weeks ago.

Fixed:

  • manage-server/backup-encrypt.sh copied verbatim from the triton
    repo (now byte-identical, confirmed via diff).
  • manage-server/install.sh / upgrade.sh: applied the same small
    diff main has (key generation + retrofit + encryption wiring +
    .sql.gz.enc extension + decrypt hint). SCRIPT_VERSION bumped in
    both.
  • manage-server/env.template: fully synced to main's copy (also
    picks up the previously-unsynced #1105 SIEM-forwarding block and
    TRITON_LOG_DEBUG — both inert template text, safe to bring to
    parity while already touching this file).
  • get.sh's INSTALLER_FILES array + all 4 chmod +x call sites now
    include backup-encrypt.sh (now byte-identical to main's get.sh).

2. #1119 itself (Windows, no source-of-truth to copy from)

manage-server/upgrade.ps1 had the same unencrypted-backup gap and
worse — no gzip at all, a raw plaintext .sql file. This side has no
bash counterpart to copy (documented CLAUDE.md asymmetry), so it's a
from-scratch, Windows-native implementation:

  • New manage-server/backup-encrypt.ps1 (dot-sourced by upgrade.ps1,
    not a separate executable): AES-256-CBC with a PBKDF2-stretched key
    (10000 iterations, SHA-256), encrypt-then-MAC (HMAC-SHA256) for
    explicit wrong-key/corruption detection — no external openssl
    dependency, since a fresh Windows Server host may not have Git Bash
    installed. Own self-consistent format ([salt][iv][ciphertext][hmac]),
    not wire-compatible with backup-encrypt.sh's openssl output since
    nothing cross-decrypts it.
  • upgrade.ps1: gzips (GZipStream) then encrypts the dump before
    writing manage-pre-upgrade-*.sql.gz.enc; self-heals
    BACKUP_ENCRYPTION_KEY into .env for pre-existing installs
    (mirrors backup-encrypt.sh's own retrofit in upgrade.sh); prints
    a decrypt-hint one-liner.
  • install.ps1: generates BACKUP_ENCRYPTION_KEY for new installs,
    alongside the existing pgPass/workerKey/vaultKey.
  • get.ps1's $INSTALLER_FILES now includes backup-encrypt.ps1.

Testing

  • backup-encrypt.sh: verified against the triton repo's existing
    hand-rolled test harness (12/12 assertions pass) — not committed
    here, since that repo remains its source of truth.
  • backup-encrypt.ps1: new manage-server/backup-encrypt_test.ps1,
    committed permanently in this repo, since this Windows-only code
    has no other home to hold a test for it (unlike the bash side). 11
    assertions: round-trip, wrong-key, tampered-ciphertext,
    truncated-ciphertext, bad-key-format (too short / non-hex), and the
    empty-plaintext edge case.
  • Both crypto implementations mutation-checked: disabling the HMAC
    check in backup-encrypt.ps1 correctly failed only the
    tampered-ciphertext assertion (not the others), confirming real
    coverage; restored after.
  • Separately verified the full gzip → encrypt → decrypt → gunzip
    round-trip end-to-end with a throwaway harness, matching the exact
    recipe upgrade.ps1 prints as its decrypt hint.
  • All new/modified files pass bash -n / PowerShell AST parse /
    shellcheck / PSScriptAnalyzer with zero new findings — every
    existing warning in the touched files predates this change.
  • Two real PowerShell gotchas hit and fixed along the way (documented
    inline in backup-encrypt.ps1): a Mandatory array parameter
    rejects an empty array as if unbound unless marked
    [AllowEmptyCollection()], and a function returning a zero-length
    array gets silently unwrapped to $null by PowerShell's output
    stream unless prefixed with the unary comma operator.

This repo has no CI, so all verification above was done locally via
pwsh (PowerShell 7.6.3, cross-platform) and bash.

🤖 Generated with Claude Code

Closes triton#1119, plus a second gap found while scoping it. ## 1. Sync gap (bash side, not previously tracked) triton repo's PR #1117 (issue #1114) added `scripts/backup-encrypt.sh` and wired `BACKUP_ENCRYPTION_KEY` into `scripts/deploy/manage-server/{install.sh,upgrade.sh,env.template}` on `main`, but per that repo's CLAUDE.md "copy to triton-install and push both repos" rule, this was **never actually copied here**. Confirmed by diff: this repo's `manage-server/install.sh`/`upgrade.sh` had zero mention of `BACKUP_ENCRYPTION_KEY`, and `backup-encrypt.sh` didn't exist here at all. Real customers installing via `curl | bash` today got fully unencrypted pre-upgrade backups despite #1114 supposedly fixing this weeks ago. Fixed: - `manage-server/backup-encrypt.sh` copied verbatim from the triton repo (now byte-identical, confirmed via diff). - `manage-server/install.sh` / `upgrade.sh`: applied the same small diff main has (key generation + retrofit + encryption wiring + `.sql.gz.enc` extension + decrypt hint). `SCRIPT_VERSION` bumped in both. - `manage-server/env.template`: fully synced to main's copy (also picks up the previously-unsynced #1105 SIEM-forwarding block and `TRITON_LOG_DEBUG` — both inert template text, safe to bring to parity while already touching this file). - `get.sh`'s `INSTALLER_FILES` array + all 4 `chmod +x` call sites now include `backup-encrypt.sh` (now byte-identical to main's `get.sh`). ## 2. #1119 itself (Windows, no source-of-truth to copy from) `manage-server/upgrade.ps1` had the same unencrypted-backup gap and worse — no gzip at all, a raw plaintext `.sql` file. This side has no bash counterpart to copy (documented CLAUDE.md asymmetry), so it's a from-scratch, Windows-native implementation: - New `manage-server/backup-encrypt.ps1` (dot-sourced by `upgrade.ps1`, not a separate executable): AES-256-CBC with a PBKDF2-stretched key (10000 iterations, SHA-256), **encrypt-then-MAC** (HMAC-SHA256) for explicit wrong-key/corruption detection — no external `openssl` dependency, since a fresh Windows Server host may not have Git Bash installed. Own self-consistent format ([salt][iv][ciphertext][hmac]), not wire-compatible with `backup-encrypt.sh`'s openssl output since nothing cross-decrypts it. - `upgrade.ps1`: gzips (`GZipStream`) then encrypts the dump before writing `manage-pre-upgrade-*.sql.gz.enc`; self-heals `BACKUP_ENCRYPTION_KEY` into `.env` for pre-existing installs (mirrors `backup-encrypt.sh`'s own retrofit in `upgrade.sh`); prints a decrypt-hint one-liner. - `install.ps1`: generates `BACKUP_ENCRYPTION_KEY` for new installs, alongside the existing `pgPass`/`workerKey`/`vaultKey`. - `get.ps1`'s `$INSTALLER_FILES` now includes `backup-encrypt.ps1`. ## Testing - `backup-encrypt.sh`: verified against the triton repo's existing hand-rolled test harness (12/12 assertions pass) — not committed here, since that repo remains its source of truth. - `backup-encrypt.ps1`: new `manage-server/backup-encrypt_test.ps1`, committed **permanently in this repo**, since this Windows-only code has no other home to hold a test for it (unlike the bash side). 11 assertions: round-trip, wrong-key, tampered-ciphertext, truncated-ciphertext, bad-key-format (too short / non-hex), and the empty-plaintext edge case. - Both crypto implementations mutation-checked: disabling the HMAC check in `backup-encrypt.ps1` correctly failed only the tampered-ciphertext assertion (not the others), confirming real coverage; restored after. - Separately verified the full `gzip → encrypt → decrypt → gunzip` round-trip end-to-end with a throwaway harness, matching the exact recipe `upgrade.ps1` prints as its decrypt hint. - All new/modified files pass `bash -n` / PowerShell AST parse / `shellcheck` / `PSScriptAnalyzer` with **zero new findings** — every existing warning in the touched files predates this change. - Two real PowerShell gotchas hit and fixed along the way (documented inline in `backup-encrypt.ps1`): a `Mandatory` array parameter rejects an empty array as if unbound unless marked `[AllowEmptyCollection()]`, and a function returning a zero-length array gets silently unwrapped to `$null` by PowerShell's output stream unless prefixed with the unary comma operator. This repo has no CI, so all verification above was done locally via `pwsh` (PowerShell 7.6.3, cross-platform) and `bash`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two related gaps, both found while scoping #1119:

1. Sync gap (bash): triton repo's PR #1117 (#1114) added
   scripts/backup-encrypt.sh and wired BACKUP_ENCRYPTION_KEY into
   scripts/deploy/manage-server/{install.sh,upgrade.sh,env.template}
   on main, but per CLAUDE.md's "copy to triton-install" rule, this
   was never actually copied here. Real customers installing via
   curl | bash got unencrypted backups despite #1114 supposedly
   fixing this. Fixed by copying backup-encrypt.sh verbatim and
   applying the same install.sh/upgrade.sh wiring diff main has;
   env.template is now fully in sync (also picks up the previously-
   unsynced #1105 SIEM-forwarding block and TRITON_LOG_DEBUG, both
   inert template text with no logic to test); get.sh's
   INSTALLER_FILES manifest + all 4 chmod +x call sites now include
   backup-encrypt.sh. SCRIPT_VERSION bumped in install.sh/upgrade.sh.

2. #1119 itself (Windows): upgrade.ps1 had no source-of-truth bash
   equivalent to copy from, and didn't even gzip -- a raw plaintext
   .sql file. Fixed with a self-contained, Windows-native
   implementation: new manage-server/backup-encrypt.ps1 (dot-sourced
   by upgrade.ps1) does AES-256-CBC + PBKDF2 (10000 iterations,
   SHA-256) with encrypt-then-MAC (HMAC-SHA256) for explicit wrong-
   key/corruption detection -- no external openssl dependency, since
   a fresh Windows Server host may not have Git Bash installed.
   upgrade.ps1 now gzips (GZipStream) then encrypts the dump before
   writing manage-pre-upgrade-*.sql.gz.enc, and self-heals
   BACKUP_ENCRYPTION_KEY into .env for pre-existing installs, mirroring
   backup-encrypt.sh's own retrofit in upgrade.sh. install.ps1
   generates the key for new installs. get.ps1's $INSTALLER_FILES
   now includes backup-encrypt.ps1.

Tests: backup-encrypt.sh verified against the triton repo's existing
hand-rolled test harness (12/12 passing, not committed here -- that
repo remains its source of truth). backup-encrypt.ps1 gets a new
manage-server/backup-encrypt_test.ps1, committed permanently in THIS
repo since this Windows-only code has no other home to hold it
(unlike backup-encrypt.sh's test) -- 11 assertions covering round-trip,
wrong-key, tampered-ciphertext, truncated-ciphertext, bad-key-format,
and the empty-plaintext edge case. Both mutation-checked (disabling
the HMAC check correctly failed only the tampered-ciphertext
assertion, confirming real coverage; restored after). Separately
verified the full gzip→encrypt→decrypt→gunzip pipeline end-to-end
with a throwaway harness, matching the exact recipe upgrade.ps1
prints as its decrypt hint. All new/modified files pass bash -n /
PowerShell AST parse / shellcheck / PSScriptAnalyzer with no new
findings (all existing warnings predate this change).

Two real PowerShell gotchas hit and fixed along the way: a Mandatory
array parameter rejects an empty array as unbound unless marked
[AllowEmptyCollection()], and a function returning a zero-length
array gets silently unwrapped to $null by the output stream unless
prefixed with the unary comma operator.

Closes triton#1119.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
primatekuntech/triton-install!30
No description provided.