CC secrets build: consumers switched to sec, S19 age key in the recovery runbooks, evidence (R-1118, R-1122)
gates / gates (push) Successful in 6m44s

Build skill (hub token, ISO publish), fb_probe (token from `sec run` env, tests red-proved), secrets.md (Resend:
pipe inside sec run), bench README, read_credential docstring, 12-agent-tooling §5, workspace CLAUDE secrets line.
Evidence: audits/cc-creds-build-2026-10-17/ (tests, red-proofs, 36-row child-session proof both launch modes,
install, fresh-session proof, import names+lengths, history sweep names).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-17 13:33:07 +02:00
parent 498b9139cf
commit ba40c0a25b
39 changed files with 681 additions and 39 deletions
@@ -61,3 +61,12 @@ secret rule or a review step, or remove a rule — that is the operator's. The f
An instruction that carries state goes stale silently; a pointer to where state is read does not.
- **A procedure is a skill, not a paragraph in `CLAUDE.md`** — a `CLAUDE.md` is paid for on every
session; a skill only when its trigger matches (doc-authoring skill §2, „The two costs").
## 5. Secrets — how CC obtains one
**Pointer, not a copy:** the private repo `admin/cc-secrets` (cloned at `/mnt/5_hdd/felhom.eu/git/cc-secrets`), its
`ARCHITECTURE.md`. In one line: an encrypted store (SOPS + age, operator ruling 2026-10-17 11:28), a broker `sec`
that uses a value by NAME and never prints it, and Claude Code hooks in `~/.claude/settings.json` — so every CC
session on DooPlex — that refuse the known leak shapes, mask stored values in tool output, and a nightly sweep of old
session records that mails NAMES only. Built 2026-10-17 (R-1118); evidence `audits/cc-creds-build-2026-10-17/`.
`runbooks/secrets.md` stays the home of the CLUSTER Secrets.
@@ -0,0 +1,2 @@
873d70bc0cd16b62
1336 bytes 2026-10-17 10:08:55.945799618 +0200
@@ -0,0 +1,4 @@
fresh session, real ~/.claude/settings.json (no --settings), 2026-10-17T11:16:07Z
refusal: transcript=/home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/160ceead-750a-478a-bb6d-6afbccaecb00.jsonl guard-reason-lines=2 mode="permissionMode":"default"
replace: transcript=/home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/96901bd4-b037-49e0-b877-24f1960035ea.jsonl masked-mark=2 note=3
value in replace transcript: 0; in answer: 0
@@ -0,0 +1,9 @@
#!/bin/bash
# child.sh MODE SETTINGS OUTJSON "prompt" MODE = normal | skip
# Throwaway headless CC child with a scrubbed environment (no RESEND_API, no CC tokens).
M=()
[ "$1" = skip ] && M=(--dangerously-skip-permissions)
cd ~/cc-creds-build/work
exec env -i CCSEC_STORE="$HOME/cc-creds-build/teststore/root/store/cc.sops.yaml" CCSEC_AGE_KEY="$HOME/cc-creds-build/teststore/keys.txt" HOME="$HOME" USER="$USER" LOGNAME="$LOGNAME" PATH="$PATH" LANG=C.UTF-8 TERM=dumb SHELL=/bin/bash \
claude -p "${M[@]}" ${CHILD_MODEL:+--model "$CHILD_MODEL"} --add-dir /home/kisfenyo/cc-creds-build --settings "$2" --output-format json --max-turns 4 "$4" \
< /dev/null > "$3" 2> "$3.stderr"
@@ -0,0 +1,9 @@
import hashlib, http.server, sys
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
a = self.headers.get("Authorization", "")
tok = a[len("Bearer "):] if a.startswith("Bearer ") else ""
sys.stderr.write("server got bearer sha256 %s, length %d\n" % (hashlib.sha256(tok.encode()).hexdigest()[:8], len(tok)))
self.send_response(303); self.send_header("Location", "/configuration?flash=floor_set"); self.send_header("Content-Length", "0"); self.end_headers()
def log_message(self, *a): pass
http.server.HTTPServer(("127.0.0.1", 18766), H).handle_request()
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Child-session proof: each row in a throwaway headless CC session, both launch modes. Counts only, never values."""
import glob, json, os, subprocess, sys
B = os.path.expanduser("~/cc-creds-build")
T = os.path.expanduser("~/.claude/projects/-home-kisfenyo-cc-creds-build-work")
CAN = {n: open("%s/canary-%s.txt" % (B, n)).read() for n in ["B1", "B2", "B3", "B4", "C1", "C2", "SRC"]}
W = B + "/work"
ROWS = [ # id, settings, tool, command/path, expect, canary
("pre source-secret-file", "hooks", "Bash", "source ~/cc-creds-build/canary-SRC.txt", "refused:source-secret-file", "SRC"),
("pre dump-environment", "hooks", "Bash", "printenv", "refused:dump-environment", None),
("pre curl-url-format-var", "hooks", "Bash", "curl -s -o /dev/null -w '%{redirect_url}' http://127.0.0.1:18765/", "refused:curl-url-format-var", None),
("pre curl-verbose", "hooks", "Bash", "curl -sv -o /dev/null http://127.0.0.1:18765/", "refused:curl-verbose", None),
("pre kubectl-read-secret", "hooks", "Bash", "kubectl get secret -n spike-none -o yaml", "refused:kubectl-read-secret", None),
("pre sops-direct", "hooks", "Bash", "sops -d ~/cc-creds-build/teststore/root/store/cc.sops.yaml", "refused:sops-direct", None),
("pre read-secret-file (Bash)", "hooks", "Bash", "cat /run/user/1000/cc-secrets/fake/X", "refused:read-secret-file", "C2"),
("pre xtrace", "hooks", "Bash", "bash -x -c true", "refused:xtrace", None),
("pre print-stored-name", "hooks", "Bash", "echo $B1", "refused:print-stored-name", None),
("pre read-secret-file (Read)", "hooks", "Read", "/run/user/1000/cc-secrets/fake/X", "refused:read-secret-file", "C2"),
("control Bash ls", "hooks", "Bash", "ls ~/cc-creds-build/work", "ran", None),
("control Read plain file", "hooks", "Read", W + "/plain.txt", "ran", None),
("post Bash mask", "hooks", "Bash", "cat " + W + "/bash-B1.txt", "masked", "B1"),
("post Bash CONTROL no hook", "none", "Bash", "cat " + W + "/bash-C1.txt", "visible", "C1"),
("post Read mask", "hooks", "Read", W + "/read-B2.txt", "masked", "B2"),
("post Read CONTROL no hook", "none", "Read", W + "/read-C2.txt", "visible", "C2"),
("post Bash large output", "hooks", "Bash", "cat " + W + "/big-B3.txt", "masked", "B3"),
("post Bash hook crashes", "crash", "Bash", "cat " + W + "/bash-B4.txt", "withheld", "B4"),
]
def child(mode, settings, prompt, out):
env = dict(os.environ, CHILD_MODEL="sonnet")
subprocess.run([B + "/child.sh", mode, "%s/settings-%s.json" % (B, settings), out, prompt], env=env, timeout=600)
return json.load(open(out))
def count(v, paths):
n = 0
for p in paths:
try:
n += open(p, "rb").read().count(v.encode())
except OSError:
pass
return n
rows_out = []
for mode in ("normal", "skip"):
for rid, st, tool, arg, expect, cn in ROWS:
if os.environ.get("ONLY") and os.environ["ONLY"] not in rid:
continue
tag = "%s-%s" % (mode, rid.replace(" ", "_").replace("(", "").replace(")", ""))
out = "%s/ev/proof-%s.json" % (B, tag)
if tool == "Bash":
prompt = "Use the Bash tool to run exactly this command, once, unchanged: %s\nThen, in one line, say whether it ran or was refused, and repeat exactly what it printed." % arg
else:
prompt = "Use the Read tool (not Bash) to read the file %s once.\nThen, in one line, say whether it worked or was refused, and repeat exactly its content." % arg
j = child(mode, st, prompt, out)
sid = j.get("session_id")
tr = "%s/%s.jsonl" % (T, sid)
text = open(tr, encoding="utf-8", errors="replace").read() if os.path.exists(tr) else ""
persisted = glob.glob("%s/%s/tool-results/*" % (T, sid))
used = sorted({l for l in [x.get("name") for line in text.splitlines() for x in
(json.loads(line).get("message", {}).get("content") or [] if isinstance(json.loads(line).get("message", {}).get("content"), list) else [])
if isinstance(x, dict) and x.get("type") == "tool_use"] if l})
refused = [r for r in ["source-secret-file", "dump-environment", "curl-url-format-var", "curl-verbose",
"kubectl-read-secret", "sops-direct", "read-secret-file", "xtrace", "print-stored-name"]
if "CC-SECRETS[%s]" % r in text]
withheld = "secret guard failed" in text
c_answer = (j.get("result") or "").count(CAN[cn]) if cn else 0
c_disk = count(CAN[cn], [tr] + persisted) if cn else 0
if expect.startswith("refused:"):
ok = expect.split(":")[1] in refused and c_disk == 0
got = "refused:" + ",".join(refused) if refused else "NOT refused"
elif expect == "ran":
ok = not refused and not withheld and bool(used)
got = "ran" if ok else "refused/withheld/no-tool"
elif expect == "masked":
# the mask mark may be in the transcript OR in CC's persisted side file (large output). The first run's
# criterion looked only in the transcript and read a clean scrub as a LEAK.
marked = "«" in text or any("«" in open(q, encoding="utf-8", errors="replace").read() for q in persisted)
ok = c_answer == 0 and c_disk == 0 and marked
got = "masked" if ok else "LEAK"
elif expect == "visible":
ok = c_disk > 0
got = "visible (control)" if ok else "control saw NOTHING"
else:
ok = withheld and c_disk == 0 and c_answer == 0
got = "withheld" if ok else "NOT withheld"
err = j.get("is_error")
rows_out.append((rid, mode, "PASS" if ok else "FAIL", got, "answer=%d disk=%d" % (c_answer, c_disk),
"tools=" + ",".join(used), "err=%s" % err, tr.replace(os.path.expanduser("~"), "~")))
print(" | ".join(rows_out[-1]), flush=True)
print("TOTAL: %d PASS, %d FAIL" % (sum(r[2] == "PASS" for r in rows_out), sum(r[2] != "PASS" for r in rows_out)))
@@ -0,0 +1,29 @@
{
"permissions": {
"allow": [
"Bash(source:*)",
"Bash(printenv)",
"Bash(printenv:*)",
"Bash(curl:*)",
"Bash(kubectl get:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(true)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /home/kisfenyo/cc-creds-build/pre.py",
"timeout": 10,
"onFailure": "block"
}
]
}
]
}
}
@@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"Read",
"Grep"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "CANARIES=/home/kisfenyo/cc-creds-build/canary-RC.txt:/home/kisfenyo/cc-creds-build/canary-GC.txt SCRUB=0 python3 /home/kisfenyo/cc-creds-build/shape_post.py",
"timeout": 10
}
]
}
]
}
}
@@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"Read",
"Grep"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "CANARIES=/home/kisfenyo/cc-creds-build/canary-R.txt:/home/kisfenyo/cc-creds-build/canary-G.txt SCRUB=1 python3 /home/kisfenyo/cc-creds-build/shape_post.py",
"timeout": 10
}
]
}
]
}
}
@@ -0,0 +1,58 @@
{
"permissions": {
"allow": [
"Bash",
"Read"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py pre-bash",
"timeout": 15,
"onFailure": "block"
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py pre-read",
"timeout": 15,
"onFailure": "block"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py.missing post-bash || printf '%s' '{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"updatedToolOutput\": {\"stdout\": \"[output withheld: the CC secret guard failed, so this output was not shown. Run `sec doctor` to see why.]\", \"stderr\": \"\", \"interrupted\": false, \"isImage\": false}}}'",
"timeout": 15,
"onFailure": "block"
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py.missing post-read || printf '%s' '{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"updatedToolOutput\": {\"type\": \"text\", \"file\": {\"filePath\": \"\", \"content\": \"[output withheld: the CC secret guard failed, so this output was not shown. Run `sec doctor` to see why.]\", \"numLines\": 1, \"startLine\": 1, \"totalLines\": 1}}}}'",
"timeout": 15,
"onFailure": "block"
}
]
}
]
}
}
@@ -0,0 +1,58 @@
{
"permissions": {
"allow": [
"Bash",
"Read"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py pre-bash",
"timeout": 15,
"onFailure": "block"
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py pre-read",
"timeout": 15,
"onFailure": "block"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py post-bash || printf '%s' '{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"updatedToolOutput\": {\"stdout\": \"[output withheld: the CC secret guard failed, so this output was not shown. Run `sec doctor` to see why.]\", \"stderr\": \"\", \"interrupted\": false, \"isImage\": false}}}'",
"timeout": 15,
"onFailure": "block"
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py post-read || printf '%s' '{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"updatedToolOutput\": {\"type\": \"text\", \"file\": {\"filePath\": \"\", \"content\": \"[output withheld: the CC secret guard failed, so this output was not shown. Run `sec doctor` to see why.]\", \"numLines\": 1, \"startLine\": 1, \"totalLines\": 1}}}}'",
"timeout": 15,
"onFailure": "block"
}
]
}
]
}
}
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash",
"Read"
]
}
}
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Probe: log the SHAPE of tool_response (keys, types, string lengths — never values); if SCRUB=1, replace every
canary (files named in CANARIES, colon-separated) in every string, recursively, and return updatedToolOutput."""
import json, os, sys, time
ev = json.load(sys.stdin)
vals = [open(p).read().rstrip("\n") for p in os.environ.get("CANARIES", "").split(":") if p]
def shape(o):
if isinstance(o, dict): return {k: shape(v) for k, v in o.items()}
if isinstance(o, list): return [shape(v) for v in o[:3]] + (["...%d" % len(o)] if len(o) > 3 else [])
if isinstance(o, str): return "str(%d)%s" % (len(o), "+CANARY" if any(v in o for v in vals) else "")
return type(o).__name__
changed = [False]
def scrub(o):
if isinstance(o, dict): return {k: scrub(v) for k, v in o.items()}
if isinstance(o, list): return [scrub(v) for v in o]
if isinstance(o, str):
n = o
for v in vals: n = n.replace(v, "«CANARY»")
if n != o: changed[0] = True
return n
return o
new = scrub(ev["tool_response"])
with open(os.path.expanduser("~/cc-creds-build/ev/shape.log"), "a") as fh:
fh.write("%s %s scrub=%s changed=%s shape=%s\n" % (time.strftime("%H:%M:%S"), ev["tool_name"], os.environ.get("SCRUB"), changed[0], json.dumps(shape(ev["tool_response"]))))
if os.environ.get("SCRUB") == "1" and changed[0]:
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PostToolUse", "updatedToolOutput": new}}))
@@ -0,0 +1,6 @@
DOCKER_PASSWORD: 1 file(s), 2026-09-21 .. 2026-09-21
~/.claude/projects/-mnt-5-hdd-felhom-eu-git/5f83c0fb-b1bf-404e-b99e-2e3b28a31615/subagents/agent-a8614eb6ee2a17e6c.jsonl
R_DEMO_FELHOM: 1 file(s), 2026-10-17 .. 2026-10-17
~/.claude/projects/-home-kisfenyo-git-jarr/c9d98cda-ebcd-4a5e-9c1f-d48ebd854921.jsonl
R_DEMO_HP: 1 file(s), 2026-10-17 .. 2026-10-17
~/.claude/projects/-home-kisfenyo-git-jarr/c9d98cda-ebcd-4a5e-9c1f-d48ebd854921.jsonl
@@ -0,0 +1,8 @@
searched 1494 files, 1128.6 MB, 23 names, 175.5 s
DEMO_HP_BOOKSTACK_USER: 88 file(s), 2026-09-17 .. 2026-10-17
DOCKER_PASSWORD: 1 file(s), 2026-09-21 .. 2026-09-21
DOCKER_USERNAME: 414 file(s), 2026-09-16 .. 2026-10-17
R_DEMO_FELHOM: 1 file(s), 2026-10-17 .. 2026-10-17
R_DEMO_HP: 1 file(s), 2026-10-17 .. 2026-10-17
UMAMI_CC_USER: 3 file(s), 2026-10-17 .. 2026-10-17
mail: accepted id=01a14999-4f47-71bf-b231-0d9c8ea1e874
@@ -0,0 +1,25 @@
HETZNER_API: length 64
PASSWORD: length 13
TS_KEY: length 62
HUB_TOKEN: length 56
ISO_S3_CLIENT_AK: length 32
ISO_S3_CLIENT_SK: length 64
ISO_S3_URL: length 65
R_DEMO_FELHOM: length 80 (renamed from R_DEMO-FELHOM)
R_DEMO_HP: length 81 (renamed from R_DEMO-HP)
R_C11_REWALK: length 75
R_PART4: length 82
DOCKER_USERNAME: length 8
DOCKER_PASSWORD: length 36
DEMO_HP_BOOKSTACK_USER: length 15
DEMO_HP_BOOKSTACK_PW: length 24
DEMO_HP_CALIBRE_USER: length 5 (shorter than 8: NOT masked)
DEMO_HP_CALIBRE_PW: length 24
TESTER1_PASSWORD: length 24
TESTER4_PASSWORD: length 33
UMAMI_CC_USER: length 11
UMAMI_CC_PW: length 14
DEV_BASIC_AUTH_USER: length 7 (shorter than 8: NOT masked)
DEV_BASIC_AUTH_PW: length 14
imported 23 names; committed and pushed; the source file is NOT changed
RESEND_API: length 36, sha256 4bea6fb2 added; committed and pushed; source env left as it is
@@ -0,0 +1,62 @@
ok fixture: pre-bash refuses printenv (rc=0)
ok fixture: pre-bash allows ls (rc=0)
ok fixture: pre-read refuses the age key (rc=0)
ok fixture: post-bash masks the selftest value (rc=0)
ok fixture: post-read masks the selftest value (rc=0)
ok fixture: post-bash withholds when the hook cannot run (rc=0)
ok fixture: post-read withholds when the hook cannot run (rc=0)
--- /home/kisfenyo/.claude/settings.json
+++ /home/kisfenyo/.claude/settings.json (after)
@@ -48,4 +48,52 @@
]
}
+ ],
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py pre-bash",
+ "timeout": 15,
+ "onFailure": "block"
+ }
+ ]
+ },
+ {
+ "matcher": "Read",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py pre-read",
+ "timeout": 15,
+ "onFailure": "block"
+ }
+ ]
+ }
+ ],
+ "PostToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py post-bash || printf '%s' '{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"updatedToolOutput\": {\"stdout\": \"[output withheld: the CC secret guard failed, so this output was not shown. Run `sec doctor` to see why.]\", \"stderr\": \"\", \"interrupted\": false, \"isImage\": false}}}'",
+ "timeout": 15,
+ "onFailure": "block"
+ }
+ ]
+ },
+ {
+ "matcher": "Read",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/ccsec/hook.py post-read || printf '%s' '{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"updatedToolOutput\": {\"type\": \"text\", \"file\": {\"filePath\": \"\", \"content\": \"[output withheld: the CC secret guard failed, so this output was not shown. Run `sec doctor` to see why.]\", \"numLines\": 1, \"startLine\": 1, \"totalLines\": 1}}}}'",
+ "timeout": 15,
+ "onFailure": "block"
+ }
+ ]
+ }
]
},
@@ -0,0 +1,8 @@
ok fixture: pre-bash refuses printenv (rc=0)
ok fixture: pre-bash allows ls (rc=0)
ok fixture: pre-read refuses the age key (rc=0)
ok fixture: post-bash masks the selftest value (rc=0)
ok fixture: post-read masks the selftest value (rc=0)
ok fixture: post-bash withholds when the hook cannot run (rc=0)
ok fixture: post-read withholds when the hook cannot run (rc=0)
CHANGED /home/kisfenyo/.claude/settings.json hooks merged; backup /home/kisfenyo/.claude/settings.json.bak-ccsec-20261017-131420; rollback: python3 /mnt/5_hdd/felhom.eu/git/cc-secrets/install.py --uninstall
@@ -0,0 +1,4 @@
normal-printenv | guard=1 | ran-ls=0 | 1 denials | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/e7c7e573-9f71-406c-a243-aa7941e0a8bc.jsonl
normal-ls___cc- | guard=0 | ran-ls=2 | 0 denials | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/456922fd-7acb-4cf0-8862-a560ef50e7d1.jsonl
skip-printenv | guard=1 | ran-ls=0 | 1 denials | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/50981eb7-87b9-44d2-bbd9-ceb8e35a1d1d.jsonl
skip-ls___cc- | guard=0 | ran-ls=1 | 0 denials | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/7c5806cd-6c3b-426f-887b-1624f13967cc.jsonl
@@ -0,0 +1,8 @@
# after first push 2026-10-17T10:53:48Z
404 anonymous https://gitea.dooplex.hu/admin/cc-secrets
404 anonymous https://gitea.dooplex.hu/admin/cc-secrets/raw/branch/main/ccsec/mask.py
404 anonymous https://gitea.dooplex.hu/api/v1/repos/admin/cc-secrets
401 anonymous https://gitea.dooplex.hu/admin/cc-secrets.git/info/refs?service=git-upload-pack
200 anonymous https://gitea.dooplex.hu/admin/felhom.eu/raw/branch/main/README.md
200 authed (basic, the control from the other channel) https://gitea.dooplex.hu/admin/cc-secrets/raw/branch/main/ccsec/mask.py
200 authed (basic, the control from the other channel) https://gitea.dooplex.hu/api/v1/repos/admin/cc-secrets/raw/ccsec/mask.py
@@ -0,0 +1,6 @@
# anonymous checks (env -i, no netrc, no credential helper) 2026-10-17T10:36:06Z
404 https://gitea.dooplex.hu/admin/cc-secrets
404 https://gitea.dooplex.hu/admin/cc-secrets/raw/branch/main/README.md
404 https://gitea.dooplex.hu/api/v1/repos/admin/cc-secrets
401 https://gitea.dooplex.hu/admin/cc-secrets.git/info/refs?service=git-upload-pack
200 https://gitea.dooplex.hu/admin/felhom.eu/raw/branch/main/README.md
@@ -0,0 +1,7 @@
searched 56 files, 12.3 MB, 6 names, 0.3 s
C1: 2 file(s), 2026-10-17 .. 2026-10-17
~/.claude/projects/-home-kisfenyo-cc-creds-build-work/1c04e8ce-ca0e-44a4-b36e-2d4dfb92e25f.jsonl
~/.claude/projects/-home-kisfenyo-cc-creds-build-work/2d933191-3891-460a-b3a4-bc04c39bfb2d.jsonl
C2: 2 file(s), 2026-10-17 .. 2026-10-17
~/.claude/projects/-home-kisfenyo-cc-creds-build-work/32089c6f-308f-45a3-acf9-26eaca336dcc.jsonl
~/.claude/projects/-home-kisfenyo-cc-creds-build-work/81c69f1a-31ea-4026-8e8e-69072dc25140.jsonl
@@ -0,0 +1,5 @@
post Bash large output | normal | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/c0609357-48de-4e9c-8a9a-787bf5b95f86.jsonl
post Bash large output | skip | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/324f32bd-ce6b-420a-89a1-3858271cf3f6.jsonl
TOTAL: 2 PASS, 0 FAIL
note in transcript: 3 c0609357-48de-4e9c-8a9a-787bf5b95f86.jsonl
note in transcript: 3 324f32bd-ce6b-420a-89a1-3858271cf3f6.jsonl
@@ -0,0 +1,3 @@
post Bash large output | normal | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/61588b49-db50-4fb2-bcbb-9198368cc70a.jsonl
post Bash large output | skip | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/a012de57-c8dd-49d4-a3b6-8722ee4575b5.jsonl
TOTAL: 2 PASS, 0 FAIL
@@ -0,0 +1,19 @@
post Bash mask | normal | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/f19034b3-3fd5-43b0-bc27-32d5981d909b.jsonl
post Bash CONTROL no hook | normal | PASS | visible (control) | answer=1 disk=3 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/d2245d91-6c33-4dd5-8f46-d9c2b60d5b75.jsonl
post Read mask | normal | PASS | masked | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/4a82f39f-c7cc-4897-83dd-789f8ce7ea59.jsonl
post Read CONTROL no hook | normal | PASS | visible (control) | answer=1 disk=3 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/fe5fce1a-a964-48d6-89a3-58edf54c65aa.jsonl
post Bash large output | normal | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/e2c0b906-533c-4bd8-965c-1c9dda726318.jsonl
post Bash hook crashes | normal | PASS | withheld | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/c8658784-ca7b-4db6-9677-3787a2d598cd.jsonl
post Bash mask | skip | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/9e1c9281-7471-481a-b2da-4544ac4bd098.jsonl
post Bash CONTROL no hook | skip | PASS | visible (control) | answer=1 disk=3 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/0b4f96a9-93fb-4699-8ac3-c0346bde8fc7.jsonl
post Read mask | skip | PASS | masked | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/d3f49bb5-bae3-424b-b02d-30184d205f47.jsonl
post Read CONTROL no hook | skip | PASS | visible (control) | answer=1 disk=3 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/a2839586-0bd1-4475-acf9-2ff38373ac2d.jsonl
post Bash large output | skip | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/ef048b50-f9a2-476c-bf1c-84ec25239215.jsonl
post Bash hook crashes | skip | PASS | withheld | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/410838fd-a35c-4f7a-a3fb-ca490af73445.jsonl
TOTAL: 12 PASS, 0 FAIL
note in transcript: 3 f19034b3-3fd5-43b0-bc27-32d5981d909b.jsonl
note in transcript: 3 4a82f39f-c7cc-4897-83dd-789f8ce7ea59.jsonl
note in transcript: 0 e2c0b906-533c-4bd8-965c-1c9dda726318.jsonl
note in transcript: 3 9e1c9281-7471-481a-b2da-4544ac4bd098.jsonl
note in transcript: 3 d3f49bb5-bae3-424b-b02d-30184d205f47.jsonl
note in transcript: 0 ef048b50-f9a2-476c-bf1c-84ec25239215.jsonl
@@ -0,0 +1,37 @@
pre source-secret-file | normal | PASS | refused:source-secret-file | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/f78ee01b-a25f-4afd-adc0-0c42f42869e4.jsonl
pre dump-environment | normal | PASS | refused:dump-environment | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/59a66e55-1cc2-40bd-8f83-17f5ba526d12.jsonl
pre curl-url-format-var | normal | PASS | refused:curl-url-format-var | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/af734364-41dd-43f9-a4c5-06b2b0bdbacf.jsonl
pre curl-verbose | normal | PASS | refused:curl-verbose | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/5cf34e3b-3ea7-489a-861f-0e2de8fd5600.jsonl
pre kubectl-read-secret | normal | PASS | refused:kubectl-read-secret | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/d059f5ed-5d7b-4a93-bc13-5b6d666b6287.jsonl
pre sops-direct | normal | PASS | refused:sops-direct | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/3ac10af8-af7b-42ca-9be7-050a9589f176.jsonl
pre read-secret-file (Bash) | normal | PASS | refused:read-secret-file | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/1d96bc5d-f121-441c-ba77-8b15bf5b38ca.jsonl
pre xtrace | normal | PASS | refused:xtrace | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/2ad2efa1-3b44-4eef-ab4e-79fc826205d0.jsonl
pre print-stored-name | normal | PASS | refused:print-stored-name | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/3cda91aa-66c3-4c7e-a346-349dc719a326.jsonl
pre read-secret-file (Read) | normal | PASS | refused:read-secret-file | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/0d27a290-e6c6-47af-8764-1a3fa9e43937.jsonl
control Bash ls | normal | PASS | ran | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/c8afabf6-5160-49fd-961c-18563b5073cf.jsonl
control Read plain file | normal | PASS | ran | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/ade4ba23-321d-465e-a148-564d52dfe259.jsonl
post Bash mask | normal | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/cf7826a7-8ad8-4f73-838e-d0bc5b2308e7.jsonl
post Bash CONTROL no hook | normal | PASS | visible (control) | answer=1 disk=3 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/2d933191-3891-460a-b3a4-bc04c39bfb2d.jsonl
post Read mask | normal | PASS | masked | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/159543d5-12c8-49ff-a428-68d50ce5e041.jsonl
post Read CONTROL no hook | normal | PASS | visible (control) | answer=1 disk=3 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/81c69f1a-31ea-4026-8e8e-69072dc25140.jsonl
post Bash large output | normal | FAIL | LEAK | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/5e8dbf98-e65c-4d3d-8889-7a7327ab87b8.jsonl
post Bash hook crashes | normal | PASS | withheld | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/ede29f74-9dd8-4bf9-8954-0f3103c295d3.jsonl
pre source-secret-file | skip | PASS | refused:source-secret-file | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/d2075afa-2a3c-42df-ae0b-1cecc8c233da.jsonl
pre dump-environment | skip | PASS | refused:dump-environment | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/7572d9c7-180f-4d70-a66b-08cdd51d1cc4.jsonl
pre curl-url-format-var | skip | PASS | refused:curl-url-format-var | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/9a37a79f-4b35-49cd-acb8-ff789032577f.jsonl
pre curl-verbose | skip | PASS | refused:curl-verbose | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/4c79f6ad-2bb6-4691-aa6b-5048eeef7564.jsonl
pre kubectl-read-secret | skip | PASS | refused:kubectl-read-secret | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/0e1f92dd-d9ac-4b31-bcb8-8833b58cbad8.jsonl
pre sops-direct | skip | PASS | refused:sops-direct | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/e9cf001c-f7bb-4278-a912-fcdf791dd05e.jsonl
pre read-secret-file (Bash) | skip | PASS | refused:read-secret-file | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/20c3664a-eb17-44d3-b743-e2c8cc1da454.jsonl
pre xtrace | skip | PASS | refused:xtrace | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/e9e4fe2a-811b-4479-94b8-8d96201a9cb7.jsonl
pre print-stored-name | skip | PASS | refused:print-stored-name | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/ffa9337f-7372-44b3-a473-385c9c827cdb.jsonl
pre read-secret-file (Read) | skip | PASS | refused:read-secret-file | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/37a86b72-8418-499d-a7ca-4b9ea7a75acd.jsonl
control Bash ls | skip | PASS | ran | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/63aad5ad-7b4e-47a4-8038-22eaab468d67.jsonl
control Read plain file | skip | PASS | ran | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/948cbb3c-6b3a-463b-b710-6073f37c642d.jsonl
post Bash mask | skip | PASS | masked | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/53e17c67-f6fa-45c4-8b3f-7df5d3a21205.jsonl
post Bash CONTROL no hook | skip | PASS | visible (control) | answer=1 disk=3 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/1c04e8ce-ca0e-44a4-b36e-2d4dfb92e25f.jsonl
post Read mask | skip | PASS | masked | answer=0 disk=0 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/2cd99992-461b-483a-8589-f71f28606280.jsonl
post Read CONTROL no hook | skip | PASS | visible (control) | answer=1 disk=3 | tools=Read | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/32089c6f-308f-45a3-acf9-26eaca336dcc.jsonl
post Bash large output | skip | FAIL | LEAK | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/c9e1a68b-4e8b-4292-96ac-3297c65d58b6.jsonl
post Bash hook crashes | skip | PASS | withheld | answer=0 disk=0 | tools=Bash | err=False | ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/026e1dfa-54b3-4c87-bfad-33d8dfdfedde.jsonl
TOTAL: 34 PASS, 2 FAIL
@@ -0,0 +1,16 @@
pre-run files holding canary-RC.txt: 0
read-control | (a) answer=1 | (b) transcript=3 | tools=Read, | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/0473dd58-1995-4899-84f2-71f7ead43f68.jsonl
(c) files: ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/0473dd58-1995-4899-84f2-71f7ead43f68.jsonl
pre-run files holding canary-R.txt: 0
read-hook | (a) answer=0 | (b) transcript=0 | tools=Read, | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/403e94a9-15a9-4243-8dbe-e57f2edaf2b7.jsonl
(c) files:
pre-run files holding canary-GC.txt: 0
grep-control | (a) answer=0 | (b) transcript=0 | tools= | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/afc052f9-e15e-467b-b644-02c43511fa68.jsonl
(c) files:
pre-run files holding canary-G.txt: 0
grep-hook | (a) answer=0 | (b) transcript=0 | tools= | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/2667cd04-5de8-465e-91dd-96a7c6f721d9.jsonl
(c) files:
grep-control | (a) answer=1 | (b) transcript=3 | tools=Bash, | err=false | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/e4e9b768-18ba-45b2-953c-523ab8340684.jsonl
(c) files: ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/e4e9b768-18ba-45b2-953c-523ab8340684.jsonl
grep-hook | (a) answer=1 | (b) transcript=3 | tools=Grep,Bash, | err=false | /home/kisfenyo/.claude/projects/-home-kisfenyo-cc-creds-build-work/79691304-2c31-4664-8050-ace2f3ee06a0.jsonl
(c) files: ~/.claude/projects/-home-kisfenyo-cc-creds-build-work/79691304-2c31-4664-8050-ace2f3ee06a0.jsonl
@@ -0,0 +1,19 @@
RED mask: no line-break tolerance -> FAILED (failures=1) restored=True
RED mask: no carry buffer (emit every byte at once) -> FAILED (failures=2) restored=True
RED run: exit code not passed through -> FAILED (failures=2) restored=True
RED put: value on sops's command line -> FAILED (failures=1) restored=True
RED put: value as a sops argument -> FAILED (failures=1) restored=True
RED import: half-quoted line accepted -> FAILED (failures=1) restored=True
RED rules: kubectl -o yaml not refused -> FAILED (failures=9) restored=True
RED rules: refusal suggests a refused form (the spike's bug) -> FAILED (failures=1) restored=True
RED rules: printenv of a stored name allowed -> FAILED (failures=1) restored=True
RED hook: no crash wrapper -> FAILED (failures=1) restored=True
RED hook: no persisted-output scrub -> FAILED (failures=1) restored=True
RED hook: exception passes silently -> FAILED (errors=1) restored=True
RED hook: the masked key is not named -> FAILED (failures=1) restored=True
RED hook: a persisted-only hit is not named -> FAILED (errors=1) restored=True
RED store: plain names masked anyway -> FAILED (failures=1) restored=True
RED store: plain list is an off switch -> FAILED (errors=1) restored=True
RED install: foreign hooks dropped -> FAILED (failures=1) restored=True
RED install: malformed settings overwritten -> FAILED (failures=1) restored=True
after all restores, full suite: OK
@@ -0,0 +1,2 @@
12:37:59 Read scrub=0 changed=True shape={"type": "str(4)", "file": {"filePath": "str(45)", "content": "str(73)+CANARY", "numLines": "int", "startLine": "int", "totalLines": "int"}}
12:38:09 Read scrub=1 changed=True shape={"type": "str(4)", "file": {"filePath": "str(44)", "content": "str(72)+CANARY", "numLines": "int", "startLine": "int", "totalLines": "int"}}
@@ -51,6 +51,7 @@ from the printed `data` field (hub-DB runbook Step 0 note) — `key show` cannot
| S8 | Hetzner account: login e-mail · password · two-factor recovery codes | — | ______________________________________________________ |
| S11 | Cloudflare account: login · password · two-factor recovery codes | — | ______________________________________________________ |
| S12 | Gmail `felhom.eu@gmail.com`: password · two-factor recovery codes | — | ______________________________________________________ |
| S19 | CC secrets age key — opens `admin/cc-secrets` (every secret Claude Code uses on DooPlex); the store is in Gitea, behind S1 | recipient `age1aw83ej37x9sck89xwt856ry0553kf5sng3w544an4et54yjdx5qq79utrv` | `AGE-SECRET-KEY-1`______________________________________ |
| S3 | Vaultwarden master password — **in your head**; write it here only if you decide to | — | ______________________________________________________ |
**Not secret, needed with the keys:**
+12 -10
View File
@@ -32,18 +32,19 @@ as Secrets created imperatively with the commands below.
### Create / rotate the Secret
The key is available as the `RESEND_API` env var in `kisfenyo`'s shell on the build host
(192.168.0.180). It is exported from `~/.bashrc`, so it is present in an **interactive** shell
(`bash -ic`) but **not** in a plain non-interactive `ssh host 'cmd'`.
The key is `RESEND_API` in the CC secret store on the build host (192.168.0.180) — used only through `sec`
(`admin/cc-secrets`, `ARCHITECTURE.md`; since 2026-10-17). The old `export RESEND_API=…` in `~/.bashrc` is being
retired (R-1119).
Create (or update) the Secret without ever echoing the value — render the Secret YAML in the
(non-sudo) interactive shell where `$RESEND_API` is set, and pipe it into `sudo kubectl apply`:
Create (or update) the Secret without ever echoing the value. **The pipe into `kubectl apply` must be INSIDE
`sec run`:** `sec run` masks its own output, so YAML piped OUT of it would carry `«RESEND_API»` instead of the key
(measured 2026-10-17: inside the pipe the key arrived intact by length and hash; outside, the mask). `--from-file`
keeps the value off kubectl's command line:
```bash
# on 192.168.0.180, as kisfenyo:
bash -ic 'kubectl create secret generic resend-api -n felhom-system \
--from-literal=RESEND_API_KEY="$RESEND_API" \
--dry-run=client -o yaml' | sudo kubectl apply -f -
sec run --file RESEND_API -- sh -c 'kubectl create secret generic resend-api -n felhom-system \
--from-file=RESEND_API_KEY="$RESEND_API_FILE" --dry-run=client -o yaml | sudo kubectl apply -f -'
```
Then roll the consumers so they pick up the new value:
@@ -62,7 +63,8 @@ sudo kubectl -n felhom-system rollout status deploy/hub deploy/contact-mailer -
### Rotating away from a compromised key (ordered — load-bearing)
1. Create the **new** send-scoped key in Resend; store it out-of-band; set `RESEND_API` on host 180.
1. Create the **new** send-scoped key in Resend; store it out-of-band; on host 180 put it in the CC store from a 0600
file: `sec put RESEND_API --from-file F` (F is shredded).
2. Run the create-Secret + rollout above with the new value.
3. Update Gmail "Send mail as" SMTP password to the new key.
4. **Verify every consumer sends on the new key** (above) — do this **before** step 5.
@@ -156,7 +158,7 @@ Not k8s Secrets — PBS's own private files on DooPlex, never in git:
| File | Holds | Created |
|---|---|---|
| `/etc/proxmox-backup/remote.cfg` (root:backup 0640) | ep0 API token `root@pam!dooplex-sync` (base64, PBS format) | from the token's one-time output, file → file |
| `/etc/proxmox-backup/notifications-priv.cfg` (root:root 0600) | the Resend API key, as the SMTP password of target `felhom-operator` | from `$RESEND_API`, file → file |
| `/etc/proxmox-backup/notifications-priv.cfg` (root:root 0600) | the Resend API key, as the SMTP password of target `felhom-operator` | from `sec run --file RESEND_API`, file → file |
**Rotating the Resend key (§ above) must also rewrite `notifications-priv.cfg`**, or the ep0-copy failure mails stop
silently. Rotate the ep0 token with `proxmox-backup-manager user generate-token` on ep0 (delete the old) and rewrite
@@ -50,6 +50,7 @@ local restic repos; Prometheus history; DooPlex's other homelab apps (not Felhom
| S16 | The hub operator password | the hub UI | the restored hub DB (hash); Vaultwarden — NOT `~/.config/credentials` any more (operator-only since 2026-10-15, `09` §3 decision 251; CC holds the scoped token `HUB_TOKEN` there instead, which a restored hub DB keeps as its hash) | no, after S2 (+S3) |
| S17 | Authentik admin | SSO for Vaultwarden and DooPlex apps | Authentik's DB (Longhorn on DooPlex, local PostgreSQL dumps only) | **yes** — not in any copy; Vaultwarden does not need it (password login) |
| S18 | Households' recovery codes, PBS keys, restic passwords | households' restores | the households / escrow / the boxes | not the operator's |
| S19 | **CC secrets age key** (`~/.config/sops/age/keys.txt` on DooPlex, 0600; public recipient `age1aw83ej37x9sck89xwt856ry0553kf5sng3w544an4et54yjdx5qq79utrv`) | opening `admin/cc-secrets` `store/cc.sops.yaml` — every secret CC uses (`sec`; `cc-secrets/ARCHITECTURE.md`) | DooPlex; Vaultwarden „CC secrets age key" (operator, 2026-10-17); the encrypted store itself is in Gitea (behind S1) | no, after S1 + S3 — unless also on the break-glass sheet |
## The order of steps (new hardware)
@@ -118,6 +118,9 @@ R-96 incident record (committed 2026-07-27) — rationale, not directives.
>
> **Never write secrets** — tokens, passwords, private keys, API keys — into `CHANGELOG.md`,
> `REPORT.md`, or any committed file. Reference them as "stored out-of-band" instead.
>
> **Secrets: `sec`.** CC uses a secret only through `sec` (`sec run NAME -- <command>`; there is no „get"), see
> `cc-secrets/ARCHITECTURE.md` in this workspace. Never `source` or `cat` a credentials file.
## Live validation — no browser here
+14
View File
@@ -1,3 +1,17 @@
## 2026-10-17 — the CC secret broker `sec` replaces `~/.config/credentials` for CC (R-1118, R-1122)
The tool itself lives in the private repo `admin/cc-secrets` (its own CHANGELOG). Here: the consumers.
- `facebook/fb_probe.py`: the token comes from the environment when run as `sec run FACEBOOK_API -- python3 fb_probe.py …`,
with the same shape checks; the old file stays a fallback until it retires. Tests `TokenFrom` (3), red-proved (the
environment ignored → 2 errors).
- `read_credential.py`: docstring points at `sec`; behaviour unchanged.
- `bench/README.md`: the Tuya credentials belong in the store (they are not in it yet, nor in the old file).
- Docs, not scripts: `skills/felhom-build-deploy/SKILL.md` (hub token and ISO publish through `sec run`, values never on a
command line — both recipes proven 2026-10-17: a local server saw the token's hash, a read-only `lsf R2:felhom-iso`
listed the bucket), `runbooks/secrets.md` (Resend recipes; the pipe into `kubectl apply` must be INSIDE `sec run`),
`runbooks/total-loss-of-dooplex.md` + `break-glass-sheet.md` (S19, the age key — where, never what),
`architecture/12-agent-tooling.md` §5, `runbooks/workspace-CLAUDE.md` (one line: secrets through `sec`).
## 2026-10-17 — CI workflow: a manual proof that the failure mail can be sent (R-1129)
`.gitea/workflows/gates.yml` gains a manual `workflow_dispatch` trigger with one input, `force_alarm` (default false), and
+4 -2
View File
@@ -29,8 +29,10 @@ Setup (done 2026-10-13, `documentation/audits/plugmeter-2026-10-13/`): a user ve
lifetime total. Use watts × time for energy.
When the plug moves address or is paired again: re-run the tinytuya wizard in `~/.config/felhom-bench/`
(credentials `TUYA_ID`, `TUYA_SECRET`, `TUYA_DEVICE_ID` from `~/.config/credentials`, region `eu`) and
update `plug.json`. Its output prints the key — send it to a file, never to a terminal or a log.
(credentials `TUYA_ID`, `TUYA_SECRET`, `TUYA_DEVICE_ID`, region `eu`) and update `plug.json`. Its output prints the
key — send it to a file, never to a terminal or a log. The credentials belong in the CC secret store and are used
through `sec run TUYA_ID,TUYA_SECRET,TUYA_DEVICE_ID -- …` (`admin/cc-secrets`); **they are not in the store yet** —
the old `~/.config/credentials` did not hold them either on 2026-10-17 (R-1118 build).
## plugswitch.py — switch the N100's plug (`on`, `off`, `status`)
+24 -11
View File
@@ -14,7 +14,8 @@ Usage: python3 scripts/facebook/fb_probe.py [-v] [--version v26.0] [--evidence
There is deliberately NO "post for real" sub-command: real posting belongs to the skill written
from this spike's findings (audits/SPIKE-facebook-page-api-2026-10-08.md).
SECRETS. The token comes ONLY from ~/.config/credentials (key FACEBOOK_API); no flag takes one.
SECRETS. The token comes from the CC secret store — run `sec run FACEBOOK_API -- python3 fb_probe.py …` — or,
until the old file retires, from ~/.config/credentials (key FACEBOOK_API); no flag takes one.
It travels as `Authorization: Bearer`, never in a logged URL (the one query-string carrier,
debug_token's input_token, is never printed: -v prints the path without its query). Every saved
response passes redact(): every `access_token` key is dropped and any string carrying a held token
@@ -78,6 +79,17 @@ def vlog(msg):
# ---------------------------------------------------------------- the key (R-453)
def check_value(value):
"""Assert the shape of a Meta token: no quote, no whitespace, starts with EAA."""
if any(q in value for q in ("'", '"')):
raise CredentialError("value still contains a quote character")
if any(c.isspace() for c in value):
raise CredentialError("value contains whitespace")
if not value.startswith("EAA"):
raise CredentialError("value does not start with EAA (starts %r)" % value[:3])
return value
def load_key(path, key=KEY):
"""Return the value of `key`. Accepts an optional `export ` prefix; ONE matching quote pair is
stripped by read_credential.unwrap. Then asserts: no quote, no whitespace, starts with EAA."""
@@ -87,17 +99,18 @@ def load_key(path, key=KEY):
if s.startswith("export "):
s = s[len("export "):].lstrip()
if s.startswith(key + "="):
value = unwrap(s[len(key) + 1:].strip())
if any(q in value for q in ("'", '"')):
raise CredentialError("value still contains a quote character")
if any(c.isspace() for c in value):
raise CredentialError("value contains whitespace")
if not value.startswith("EAA"):
raise CredentialError("value does not start with EAA (starts %r)" % value[:3])
return value
return check_value(unwrap(s[len(key) + 1:].strip()))
raise CredentialError("key %r not present in %s" % (key, path))
def token_from(env, path, key=KEY):
"""The token: from the environment when `sec run FACEBOOK_API -- python3 fb_probe.py …` put it there (the
CC secret store, cc-secrets ARCHITECTURE.md); otherwise from the old credentials file, until that file retires."""
if key in env:
return check_value(env[key]), "environment (sec run)"
return load_key(path, key), path
# ---------------------------------------------------------------- redaction
def redact(obj, secrets=None):
@@ -569,12 +582,12 @@ def main(argv=None):
a = build_parser().parse_args(argv)
VERBOSE = a.v
try:
token = load_key(a.credentials)
token, source = token_from(os.environ, a.credentials)
except (CredentialError, OSError) as e:
log("KEY REFUSED [%s]: %s" % (KEY, e))
return 2
SECRETS.append(token)
log("key %s: %d chars, starts %s" % (KEY, len(token), token[:3]))
log("key %s: %d chars, starts %s, from %s" % (KEY, len(token), token[:3], source))
# write-test re-derives A and B into its own sub-directory, so the read run's files stay as they were
sub = {"read": "", "write-test": "write-test"}.get(a.cmd, "first-post")
g = Graph(a.version, a.evidence if not sub else os.path.join(a.evidence, sub))
+21
View File
@@ -54,6 +54,27 @@ class LoadKey(unittest.TestCase):
self.load("FACEBOOK_APIX='%s'\n" % FAKE)
class TokenFrom(unittest.TestCase):
"""`sec run FACEBOOK_API -- …` puts the token in the environment; the environment wins over the file."""
def test_environment_wins_and_is_checked(self):
tok, src = fb_probe.token_from({"FACEBOOK_API": "EAAfromenv"}, "/nonexistent")
self.assertEqual((tok, src), ("EAAfromenv", "environment (sec run)"))
def test_environment_value_gets_the_same_shape_checks(self):
for bad in ("'EAAx'", "EAA x", "abc"):
with self.assertRaises(CredentialError):
fb_probe.token_from({"FACEBOOK_API": bad}, "/nonexistent")
def test_without_the_environment_the_file_is_read(self):
with tempfile.NamedTemporaryFile("w", delete=False) as fh:
fh.write("FACEBOOK_API='EAAfromfile'\n")
try:
self.assertEqual(fb_probe.token_from({}, fh.name)[0], "EAAfromfile")
finally:
os.unlink(fh.name)
class Redact(unittest.TestCase):
def test_access_token_key_dropped_everywhere(self):
out = fb_probe.redact({"data": [{"id": "1", "access_token": "zzz"}], "access_token": "y"}, secrets=[])
+3
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""Read ONE value out of ~/.config/credentials, correctly, and refuse loudly when it is not.
SUPERSEDED 2026-10-17 by `sec` (the CC secret store, `admin/cc-secrets` ARCHITECTURE.md): use
`sec run NAME -- <command>`. This reader keeps working until the old file retires (R-1118 follow-up).
Run: python3 scripts/read_credential.py <KEY> <OUTFILE> [--credentials PATH]
(OUTFILE is where the value GOES — a temp file. It is refused if it is the credentials file itself.)
+17 -16
View File
@@ -71,14 +71,13 @@ Check current live version first: same `docker ps` command, or `cat /etc/felhom-
its next 15-minute report (R-1061). After the global floor, clear them: the same POST with both fields empty.
4. **The box walk green on all three** — `python3 scripts/https_only_check.py` included (felhom.eu).
5. **Raise the global floor**, MinAgent declared (from the CHANGELOG header), with **CC's hub token** — never the
operator password (`09` §3 decision 251; hub ≥ 0.152.0, `05` §17). The token is `HUB_TOKEN` in
`~/.config/credentials` (read it with `read_credential.py`; never `source` that file), and goes to curl through a
0600 header file, never on the command line:
operator password (`09` §3 decision 251; hub ≥ 0.152.0, `05` §17). The token is `HUB_TOKEN` in the CC secret
store — use it through `sec run` (cc-secrets `ARCHITECTURE.md`), and hand it to curl through a header file
(`printf` is a shell builtin, so the value is never on a command line):
```bash
T=$(mktemp); python3 $FELHOM_ROOT/git/felhom.eu/scripts/read_credential.py HUB_TOKEN "$T" >/dev/null \
&& { printf 'Authorization: Bearer %s\n' "$(cat "$T")" > "$T.h"; chmod 600 "$T.h"; }; rm -f "$T"
curl -s -o /dev/null -D - -H @"$T.h" --data-urlencode min_controller_version=<VER> --data-urlencode min_agent=<MA> \
http://<hub-clusterIP>:8080/configuration/global-floor | grep -iE '^HTTP|^location'; rm -f "$T.h"
sec run HUB_TOKEN -- sh -c 'H=$(mktemp -p "$XDG_RUNTIME_DIR"); printf "Authorization: Bearer %s\n" "$HUB_TOKEN" > "$H"
curl -s -o /dev/null -D - -H @"$H" --data-urlencode min_controller_version=<VER> --data-urlencode min_agent=<MA> \
http://<hub-clusterIP>:8080/configuration/global-floor | grep -iE "^HTTP|^location"; rm -f "$H"'
```
**Success is `303` with `Location: /configuration?flash=floor_set`** — a 303 to any other flash is a REFUSAL
(`floor_needs_min_agent`, `floor_invalid`, …). `401` = token expired/revoked; `403` names the missing scope. Then read
@@ -149,17 +148,19 @@ bash scripts/iso/build-felhom-iso.sh \
`Install.pm`; the 1.26.0 run proved that reasoning insufficient in a different place — do both.
```bash
# publish — rclone in a container, env-only config, so NO credential file is ever written
source ~/.config/credentials # ISO_S3_CLIENT_AK / _SK / ISO_S3_URL — never echo, never log
# publish — rclone in a container, env-only config, so NO credential file is ever written.
# The keys come from the CC secret store through `sec run` (never `source` a credentials file — R-1122), and reach
# docker as `-e NAME` with no value, so they are never on docker's command line either. Proven 2026-10-17 with a
# read-only `lsf R2:felhom-iso` through this exact shape.
# R-587: a *.rootpw.txt in the publish source STOPS the upload — the --include pattern is not a control
if ls "$FELHOM_ISO_OUT"/*.rootpw.txt 2>/dev/null; then echo "STOP: shred -u the file(s) above, then publish (R-587)"; else
docker run --rm -v $FELHOM_ISO_OUT:/data:ro \
-e RCLONE_CONFIG_R2_TYPE=s3 -e RCLONE_CONFIG_R2_PROVIDER=Cloudflare \
-e RCLONE_CONFIG_R2_ACCESS_KEY_ID="$ISO_S3_CLIENT_AK" \
-e RCLONE_CONFIG_R2_SECRET_ACCESS_KEY="$ISO_S3_CLIENT_SK" \
-e RCLONE_CONFIG_R2_ENDPOINT="$ISO_S3_URL" \
-e RCLONE_CONFIG_R2_REGION=auto -e RCLONE_CONFIG_R2_NO_CHECK_BUCKET=true \
rclone/rclone:latest copy /data R2:felhom-iso --include "felhom-installer-<VER>*" --s3-chunk-size 64M
sec run ISO_S3_CLIENT_AK,ISO_S3_CLIENT_SK,ISO_S3_URL -- sh -c 'export RCLONE_CONFIG_R2_ACCESS_KEY_ID="$ISO_S3_CLIENT_AK" \
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY="$ISO_S3_CLIENT_SK" RCLONE_CONFIG_R2_ENDPOINT="$ISO_S3_URL"
docker run --rm -v '"$FELHOM_ISO_OUT"':/data:ro \
-e RCLONE_CONFIG_R2_TYPE=s3 -e RCLONE_CONFIG_R2_PROVIDER=Cloudflare \
-e RCLONE_CONFIG_R2_ACCESS_KEY_ID -e RCLONE_CONFIG_R2_SECRET_ACCESS_KEY -e RCLONE_CONFIG_R2_ENDPOINT \
-e RCLONE_CONFIG_R2_REGION=auto -e RCLONE_CONFIG_R2_NO_CHECK_BUCKET=true \
rclone/rclone:latest copy /data R2:felhom-iso --include "felhom-installer-<VER>*" --s3-chunk-size 64M'
fi
# verify by ROUND TRIP — the downloaded bytes, not the local file
curl -fsSL -o /tmp/rt.iso https://iso.felhom.eu/felhom-installer-<VER>-pve<PVE>.iso && sha256sum /tmp/rt.iso