hub v0.127.0: off-site key registrar (box never gets the storage password), password sealed at rest, daily key check, clean-up window (shipped off) — decisions 68-69, R-820/R-821/R-822
gates / gates (push) Successful in 29s
gates / gates (push) Successful in 29s
Part A evidence (migration spike, sftp-written repo through the pinned rclone key) and the hub red-proofs under documentation/audits/offsite-lock-build-2026-10-03/. Manifest bump follows after the image is built and Secret/offsite-secret-key exists. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
## 1. TODAY's transport (sftp:, unpinned key)
|
||||
$ sftp.sh init
|
||||
created restic repository 8becf8f1de at sftp:u629488-sub4@u629488-sub4.your-storagebox.de:spike-migrate
|
||||
|
||||
Please note that knowledge of your password is required to access
|
||||
the repository. Losing your password means that your data is
|
||||
irrecoverably lost.
|
||||
[rc=0]
|
||||
|
||||
$ sftp.sh backup /d/tree --host migbox --tag app1
|
||||
no parent snapshot found, will read all files
|
||||
|
||||
Files: 3 new, 0 changed, 0 unmodified
|
||||
Dirs: 3 new, 0 changed, 0 unmodified
|
||||
Added to the repository: 392.552 KiB (392.119 KiB stored)
|
||||
|
||||
processed 3 files, 390.639 KiB in 0:02
|
||||
snapshot 9c767903 saved
|
||||
[rc=0]
|
||||
|
||||
$ sftp.sh backup /d/tree --host migbox --tag app1
|
||||
using parent snapshot 9c767903
|
||||
|
||||
Files: 0 new, 1 changed, 2 unmodified
|
||||
Dirs: 0 new, 3 changed, 0 unmodified
|
||||
Added to the repository: 1.929 KiB (1.328 KiB stored)
|
||||
|
||||
processed 3 files, 390.641 KiB in 0:01
|
||||
snapshot 761dc658 saved
|
||||
[rc=0]
|
||||
|
||||
$ sftp.sh snapshots
|
||||
ID Time Host Tags Paths
|
||||
--------------------------------------------------------------
|
||||
9c767903 2026-10-03 14:04:12 migbox app1 /d/tree
|
||||
761dc658 2026-10-03 14:04:18 migbox app1 /d/tree
|
||||
--------------------------------------------------------------
|
||||
2 snapshots
|
||||
[rc=0]
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
## 2. key line now PINNED (same key). 3. rclone: through it
|
||||
control: today's sftp: transport through the pinned key:
|
||||
$ sftp.sh snapshots
|
||||
Fatal: unable to open repository at sftp:u629488-sub4@u629488-sub4.your-storagebox.de:spike-migrate: unable to start the sftp session, error: unexpected EOF
|
||||
[rc=1]
|
||||
|
||||
$ rc.sh snapshots
|
||||
ID Time Host Tags Paths
|
||||
--------------------------------------------------------------
|
||||
9c767903 2026-10-03 14:04:12 migbox app1 /d/tree
|
||||
761dc658 2026-10-03 14:04:18 migbox app1 /d/tree
|
||||
--------------------------------------------------------------
|
||||
2 snapshots
|
||||
[rc=0]
|
||||
|
||||
$ rc.sh backup /d/tree --host migbox --tag app1
|
||||
using parent snapshot 761dc658
|
||||
|
||||
Files: 0 new, 0 changed, 3 unmodified
|
||||
Dirs: 0 new, 0 changed, 3 unmodified
|
||||
Added to the repository: 0 B (0 B stored)
|
||||
|
||||
processed 3 files, 390.641 KiB in 0:00
|
||||
snapshot 56667008 saved
|
||||
[rc=0]
|
||||
|
||||
$ rc.sh restore 9c767903 --target /d/restored --include /d/tree/sub/note.txt
|
||||
restoring <Snapshot 9c767903 of [/d/tree] at 2026-10-03 14:04:12.212018962 +0000 UTC by root@migbox> to /d/restored
|
||||
[rc=0]
|
||||
|
||||
restored sftp-era file == its content at that snapshot: IDENTICAL
|
||||
$ rc.sh check
|
||||
using temporary cache in /tmp/restic-check-cache-456083153
|
||||
create exclusive lock for repository
|
||||
load indexes
|
||||
check all packs
|
||||
check snapshots, trees and blobs
|
||||
[0:00] 100.00% 3 / 3 snapshots
|
||||
|
||||
no errors were found
|
||||
[rc=0]
|
||||
|
||||
$ rc.sh check --read-data
|
||||
using temporary cache in /tmp/restic-check-cache-2313441632
|
||||
create exclusive lock for repository
|
||||
load indexes
|
||||
check all packs
|
||||
check snapshots, trees and blobs
|
||||
[0:00] 100.00% 3 / 3 snapshots
|
||||
|
||||
read all data
|
||||
[0:00] 100.00% 4 / 4 packs
|
||||
|
||||
no errors were found
|
||||
[rc=0]
|
||||
|
||||
## 4.
|
||||
$ rc.sh forget 9c767903
|
||||
unable to remove <snapshot/9c767903c1> from the repository
|
||||
[0:48] 0.00% 0 / 1 files deleted
|
||||
|
||||
blob not removed, server response: 403 Forbidden (403)
|
||||
[rc=1]
|
||||
|
||||
$ rc.sh snapshots
|
||||
ID Time Host Tags Paths
|
||||
--------------------------------------------------------------
|
||||
9c767903 2026-10-03 14:04:12 migbox app1 /d/tree
|
||||
761dc658 2026-10-03 14:04:18 migbox app1 /d/tree
|
||||
56667008 2026-10-03 14:04:51 migbox app1 /d/tree
|
||||
--------------------------------------------------------------
|
||||
3 snapshots
|
||||
[rc=0]
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
## E1: SAME key twice — append-only line FIRST, deleting line SECOND
|
||||
$ rc.sh forget 56667008
|
||||
unable to remove <snapshot/5666700865> from the repository
|
||||
[0:48] 0.00% 0 / 1 files deleted
|
||||
|
||||
blob not removed, server response: 403 Forbidden (403)
|
||||
[rc=1]
|
||||
|
||||
## E2: SAME key twice — deleting line FIRST
|
||||
$ rc.sh forget 56667008 --dry-run
|
||||
Would have removed the following snapshots:
|
||||
{56667008}
|
||||
|
||||
[rc=0]
|
||||
|
||||
## E3: a SECOND key (window key) on its own deleting line, the box key stays pinned
|
||||
$ rcw.sh forget 56667008
|
||||
[0:00] 100.00% 1 / 1 files deleted
|
||||
|
||||
[rc=0]
|
||||
|
||||
$ rc.sh snapshots
|
||||
ID Time Host Tags Paths
|
||||
--------------------------------------------------------------
|
||||
9c767903 2026-10-03 14:04:12 migbox app1 /d/tree
|
||||
761dc658 2026-10-03 14:04:18 migbox app1 /d/tree
|
||||
--------------------------------------------------------------
|
||||
2 snapshots
|
||||
[rc=0]
|
||||
|
||||
## E3b: pinned key still refused while the window key exists
|
||||
$ rc.sh forget 761dc658
|
||||
[0:48] 0.00% 0 / 1 files deleted
|
||||
|
||||
unable to remove <snapshot/761dc6583f> from the repository
|
||||
blob not removed, server response: 403 Forbidden (403)
|
||||
[rc=1]
|
||||
|
||||
## E2 (real): SAME key, deleting line FIRST — a real forget
|
||||
$ rc.sh forget 761dc658
|
||||
[0:00] 100.00% 1 / 1 files deleted
|
||||
|
||||
[rc=0]
|
||||
|
||||
$ rc.sh snapshots
|
||||
ID Time Host Tags Paths
|
||||
--------------------------------------------------------------
|
||||
9c767903 2026-10-03 14:04:12 migbox app1 /d/tree
|
||||
--------------------------------------------------------------
|
||||
1 snapshots
|
||||
[rc=0]
|
||||
|
||||
## E2 closed: deleting line removed again -> refused
|
||||
$ rc.sh forget 9c767903
|
||||
[0:48] 0.00% 0 / 1 files deleted
|
||||
|
||||
unable to remove <snapshot/9c767903c1> from the repository
|
||||
blob not removed, server response: 403 Forbidden (403)
|
||||
[rc=1]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
## Part A teardown 2026-10-03T14:09:25Z
|
||||
authorized_keys sha256 now 795e715315973740 / orig 795e715315973740
|
||||
home: . .. .config .ssh felhom-repo spike-migrate
|
||||
spike-migrate KEPT on purpose (1 snapshot) for Part E's planted history; .config/rclone is the provider rclone's
|
||||
@@ -0,0 +1,12 @@
|
||||
## pinned path ABSOLUTE (/home/spike-migrate, the descriptor's form)
|
||||
ID Time Host Tags Paths
|
||||
--------------------------------------------------------------
|
||||
9c767903 2026-10-03 14:04:12 migbox app1 /d/tree
|
||||
--------------------------------------------------------------
|
||||
1 snapshots
|
||||
## probe: ssh with the pinned key, stdin closed
|
||||
2026/10/03 14:11:14 NOTICE: Config file "/home/.config/rclone/rclone.conf" not found - using defaults
|
||||
rc=0
|
||||
## probe with an UNPINNED key (control)
|
||||
Command not found. Use 'help' to get a list of available commands.
|
||||
rc=8
|
||||
@@ -0,0 +1,9 @@
|
||||
## cat config on a repo that does not exist (pinned key)
|
||||
Fatal: unable to open config file: <config/> does not exist
|
||||
Is there a repository at the following location?
|
||||
rclone:spike-migrate
|
||||
rc=1
|
||||
## wrong key (window key not in the file) — transport failure shape
|
||||
rclone: u629488-sub4@u629488-sub4.your-storagebox.de: Permission denied (publickey,password).
|
||||
Fatal: unable to open repository at rclone:spike-migrate: error talking HTTP to rclone: Get "http://localhost/file-5577006791947779410": unexpected EOF
|
||||
rc=1
|
||||
@@ -0,0 +1,12 @@
|
||||
# Part A exit test — written before any command (2026-10-03 evening)
|
||||
|
||||
Venue: `u629488-sub4` (tester-1), repo dir `spike-migrate` only. restic 0.14.0 from controller image 0.288.0.
|
||||
|
||||
1. A repo is created and backed up TODAY'S way: `sftp:` transport, an UNPINNED key, port 23 — 2 snapshots.
|
||||
2. The key line is then replaced by the PINNED line (`command="rclone serve restic --stdio --append-only spike-migrate",restrict`).
|
||||
3. Through the pinned key over `rclone:` (`-o rclone.program="ssh -p 23 … -i <key> … rclone"`), MUST succeed:
|
||||
`snapshots` (both sftp-era snapshots listed), `backup` (count 2 → 3, parent = the sftp-era snapshot),
|
||||
`restore` of one file from an sftp-era snapshot (bytes identical), `check` (exclusive lock taken and released),
|
||||
`check --read-data` (the integrity job's full depth).
|
||||
4. Through the pinned key MUST be refused: `forget <sftp-era id>` (403). Count stays 3.
|
||||
5. Fail → stop and report; no build.
|
||||
@@ -0,0 +1,22 @@
|
||||
## RP1: SaveOneTimeSecret without sealing (the pre-v0.127.0 behaviour)
|
||||
=== RUN TestOffsiteSecret_RawRowHoldsNoPassword
|
||||
offsite_secret_seal_test.go:39: raw row is not sealed: "Sup3rSecretPw%"
|
||||
--- FAIL: TestOffsiteSecret_RawRowHoldsNoPassword (0.02s)
|
||||
|
||||
## RP2: consume handler serving the password again (the pre-v0.127.0 handler)
|
||||
=== RUN TestConsumePassword_RetiredReturnsNoPassword
|
||||
offsite_test.go:43: consume → 200, want 410 (retired)
|
||||
--- FAIL: TestConsumePassword_RetiredReturnsNoPassword (0.02s)
|
||||
|
||||
## RP3: audit that ignores the pin (every line counted as pinned)
|
||||
=== RUN TestInstall_MigratesUnpinnedKeyAndAuditGoesClean
|
||||
offsitekeys_test.go:98: before: {Lines:2 Pinned:2 Findings:[]} <nil> — want 2 unpinned findings (the decoy must be seen)
|
||||
--- FAIL: TestInstall_MigratesUnpinnedKeyAndAuditGoesClean (0.00s)
|
||||
=== RUN TestWindow_PrependAuditClose
|
||||
offsitekeys_test.go:171: a window line with no open window must alarm: {Lines:2 Pinned:2 Findings:[]}
|
||||
--- FAIL: TestWindow_PrependAuditClose (0.00s)
|
||||
|
||||
## restored — all green:
|
||||
ok gitea.dooplex.hu/admin/felhom-hub/internal/store 2.888s
|
||||
ok gitea.dooplex.hu/admin/felhom-hub/internal/api 4.047s
|
||||
ok gitea.dooplex.hu/admin/felhom-hub/internal/offsitekeys 0.009s
|
||||
@@ -0,0 +1,21 @@
|
||||
## RPC1: retention ignores the pin (the pre-v0.289.0 'forget --prune after every run')
|
||||
=== RUN TestRetention_PinnedWithoutWindowDeletesNothing
|
||||
offbox_window_test.go:106: a forget ran without a window: [[forget --group-by host,tags --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune] [forget --group-by host,tags --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune]]
|
||||
--- FAIL: TestRetention_PinnedWithoutWindowDeletesNothing (0.00s)
|
||||
=== RUN TestRunOffboxBackup_PinnedNeverForgetsWithoutWindow
|
||||
offbox_window_test.go:127: the pinned run reached forget 1 time(s)
|
||||
--- FAIL: TestRunOffboxBackup_PinnedNeverForgetsWithoutWindow (0.00s)
|
||||
|
||||
## RPC2: guard without the future-date check
|
||||
=== RUN TestOffsiteGuard_LabThirteenFutureFakes_Refused
|
||||
offbox_window_test.go:154: window close = [{ID:7 CountBefore:16 CountAfter:16 Removed:0 Outcome:guard-refused Reason:the policy would remove snapshot r1 from 2026-10-03T12:45:38Z — younger than 8 days, which honest retention never does}]
|
||||
--- FAIL: TestOffsiteGuard_LabThirteenFutureFakes_Refused (0.00s)
|
||||
|
||||
## RPC3: bridge trusts the registrar without proving the pin
|
||||
=== RUN TestBridge_RegisteredButNotPinnedRefuses
|
||||
offsiteapply_test.go:212: a key that does not reach the pinned server must refuse
|
||||
--- FAIL: TestBridge_RegisteredButNotPinnedRefuses (0.00s)
|
||||
|
||||
## restored:
|
||||
ok gitea.dooplex.hu/admin/felhom-controller/internal/backup 433.945s
|
||||
ok gitea.dooplex.hu/admin/felhom-controller/internal/offsiteapply (cached)
|
||||
@@ -117,6 +117,29 @@ shred -u /path/to/keyfile
|
||||
|
||||
---
|
||||
|
||||
## Off-site password sealing key — `Secret/offsite-secret-key` (hub v0.127.0, decision 69, R-821)
|
||||
|
||||
**What uses it:** `hub` env `OFFSITE_SECRET_KEY` (64 hex characters = 32 bytes). It seals every Storage Box
|
||||
sub-account password in `one_time_secrets.value`; the hub's key registrar opens them to write box keys.
|
||||
**Required** — the pod does not start without it.
|
||||
|
||||
**Create (once, before the first v0.127.0 sync) — the value never touches a file or the terminal:**
|
||||
|
||||
```bash
|
||||
sudo kubectl -n felhom-system create secret generic offsite-secret-key \
|
||||
--from-literal=OFFSITE_SECRET_KEY="$(openssl rand -hex 32)"
|
||||
```
|
||||
|
||||
**If it is lost:** the sealed passwords cannot be opened. Nothing on the boxes breaks (their keys are installed);
|
||||
the registrar and the daily check fail with `offsite_key_audit_failed`. Recover per customer with the hub's
|
||||
**Re-issue offsite credentials** button (the provider resets the password; the hub seals the new one). Keep a copy
|
||||
in the operator's password manager if a Re-issue round is not acceptable.
|
||||
|
||||
**Rotation:** not built. A new key cannot open the old rows; rotate by setting the new key and pressing Re-issue
|
||||
for every off-site customer.
|
||||
|
||||
---
|
||||
|
||||
## Other committed secrets (tracked, NOT yet de-gitted — backlog)
|
||||
|
||||
`manifests/felhom.secret.yaml` still commits other plaintext secrets (`healthchecks-config` `SECRET_KEY`
|
||||
|
||||
Reference in New Issue
Block a user