Design — #1553: Centralny Ops Audit Log
Status: PLAN READY — gotowy do implementacji w worktree Issue: #1553 Autor: Claude Sonnet 4.6 (orchestrator) Design zatwierdzony: 2026-06-27 Review (/review-plan) zatwierdzony: 2026-06-27
Podjęte decyzje
| # | Decyzja | Wynik |
|---|---|---|
| D1 | detail format | text — prosty, bezpieczny |
| D3 | GH Actions | Tak — secrets-sync.yml loguje deploy operacje |
| D5 | Lokalne sesje | Opcjonalne — log_op() silent-skip gdy brak env |
| D6 | Retroaktywne | Od rotacji 2026-06-26/27 (secrets-rotation-log.md) |
| D7 | Grafana | Nowy dedykowany dashboard “Infra Operations Audit” |
| D-new | session_start/end | Tylko dla nie-agentowych procesów (GH Actions, cron) |
| D-retention | Retencja | Supabase: 2 mies. hot; Wasabi: archiwum JSON Lines; brak raportu |
Kontekst i relacja do istniejących tabel
dev_r_agent_sessions — śledzi kto uruchomił sesję, kiedy, jaki skill, wynik.
infra_operations — śledzi co konkretnie zrobiono na infrastrukturze.
dev_r_agent_sessions infra_operations
──────────────────── ──────────────────────────────────────
sess_abc123 (IO1) ──► session_id credential_rotation / SUPABASE_ACCESS_TOKEN / success
└── subagent ──► session_id service_restart / monitoring-grafana-1 / success
gh-actions/secrets-sync ──► session_start / gh-actions/secrets-sync / success
──► deploy / monitoring/.env@vps-i1 / success
──► session_end / gh-actions/secrets-sync / success
session_id text — miękkie powiązanie (nie FK). Sesje Claude mają dev_r_agent_sessions row;
nieagentowe procesy (GH Actions, cron) nie mają — dla nich session_start/end w infra_operations
jest jedynym śladem.
Schema (finalna)
CREATE TABLE infra_operations (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ts timestamptz DEFAULT now() NOT NULL,
actor text NOT NULL, -- 'claude-runner@vps-i1' | 'claude-local' | 'gh-actions/secrets-sync'
session_id text, -- Claude session ID lub GH Actions run ID
issue_ref text, -- '#1503'
op_type text NOT NULL, -- see op_type catalogue
resource text NOT NULL, -- 'SUPABASE_SERVICE_ROLE_KEY' | 'monitoring-grafana-1'
target text, -- 'vps-i1' | 'bms-4' | 'github_secrets' | 'sops:monitoring'
result text NOT NULL, -- 'success' | 'failed' | 'skipped' | 'deferred'
detail text -- 'rotated, len=44, prefix=sbp_68fd' — NIGDY wartość sekretu
);
-- RLS (obowiązkowe per CLAUDE.md §RLS)
ALTER TABLE infra_operations ENABLE ROW LEVEL SECURITY;
CREATE POLICY service_role_all ON infra_operations
FOR ALL TO service_role USING (true) WITH CHECK (true);
CREATE POLICY grafana_readonly_select ON infra_operations
FOR SELECT TO grafana_readonly USING (true);
-- Indeksy
CREATE INDEX idx_infra_ops_ts ON infra_operations (ts DESC);
CREATE INDEX idx_infra_ops_actor_ts ON infra_operations (actor, ts DESC);
CREATE INDEX idx_infra_ops_op_type ON infra_operations (op_type, ts DESC);Retencja i archiwizacja
Architektura
ZAPIS POST do Supabase (service_role)
│
HOT WINDOW Supabase infra_operations — ostatnie 2 miesiące
│
ARCHIWIZACJA audit-engine action: infra_ops_archive
co poniedziałek 03:00 UTC:
1. SELECT * WHERE ts < now() - interval '8 weeks'
2. Dopisz do Wasabi: infra-ops/YYYY-MM.jsonl (JSON Lines, append)
3. DELETE z Supabase po sukcesie zapisu
│
COLD ARCHIVE Wasabi s3://p24-infra/infra-ops/
Format: YYYY-MM.jsonl (jeden plik = jeden miesiąc)
Retencja: bez limitu (Wasabi tani storage)
audit-engine action (INSERT do audit.actions)
INSERT INTO audit.actions (project_id, action_type, schedule, status, config)
VALUES (
'p24-infra',
'infra_ops_archive',
'0 3 * * 1', -- poniedziałek 03:00 UTC
'active',
'{
"wasabi_bucket": "p24-infra",
"wasabi_prefix": "infra-ops/",
"retention_weeks": 8,
"supabase_table": "infra_operations"
}'::jsonb
);Nowy action handler: audit-engine/actions/infra_ops_archive.py.
Grafana a historia
Grafana dashboard odpytuje Supabase (hot window ≤ 2 mies.). Dla danych historycznych:
pobierz ręcznie z Wasabi: aws s3 cp s3://p24-infra/infra-ops/2026-06.jsonl .
(Wasabi obsługuje AWS CLI z endpointem s3.eu-central-2.wasabisys.com).
Katalog op_type
Lifecycle sesji (tylko nieagentowe procesy — GH Actions, cron, skrypty)
Sesje Claude NIE logują
session_start/endtutaj — majądev_r_agent_sessions.
| op_type | resource przykład | Kiedy logować |
|---|---|---|
session_start | gh-actions/secrets-sync | Start GH Actions job lub cron skryptu |
session_end | gh-actions/secrets-sync | Koniec jobu (result: success/failed) |
Admin (sysadmin)
| op_type | resource przykład | Kiedy logować |
|---|---|---|
credential_rotation | SUPABASE_SERVICE_ROLE_KEY | Rotacja każdego credentialu |
sops_edit | sops:monitoring.env.sops | Edycja i re-encrypt pliku SOPS |
ssh_session | root@vps-i1 | Otwarcie sesji SSH |
firewall_change | ufw@bms-2 | Zmiana reguły ufw/iptables |
user_management | claude-runner@bms-4 | Tworzenie/usuwanie użytkowników OS/DB |
ssl_cert_renewal | *.vps-i1.infra.zintegrowana.online | Odnowienie certyfikatu |
backup_restore | p24-infra/thanos@wasabi | Przywrócenie backupu |
server_provision | vps-i2 | Provisioning nowego serwera |
dns_change | grafana.vps-i1.infra.zintegrowana.online | Zmiana rekordu DNS |
DevOps
| op_type | resource przykład | Kiedy logować |
|---|---|---|
service_restart | monitoring-grafana-1 | Restart kontenera/serwisu |
service_start | monitoring-prometheus-1 | Start serwisu |
service_stop | monitoring-caddy-1 | Zatrzymanie serwisu |
deploy | monitoring/.env@vps-i1 | Deployment aktualizacji |
config_change | monitoring/prometheus.yml | Zmiana konfiguracji |
docker_pull | grafana/grafana:latest | Pull nowego obrazu |
container_cleanup | docker-prune@vps-i1 | Sprzątanie kontenerów/obrazów |
alert_silence | alertmanager@vps-i1 | Wyciszenie alertu |
incident_response | monitoring-stack@vps-i1 | Ręczna interwencja |
Developer
| op_type | resource przykład | Kiedy logować |
|---|---|---|
migration | supabase:027_dev_r_agent_sessions | Migracja Supabase/DB |
gh_pr_create | radieu/p24-infra#1560 | Tworzenie PR |
gh_pr_merge | radieu/p24-infra#1560 | Merge PR |
code_deploy | monitoring-exporters@vps-i1 | Deployment kodu przez CI |
gh_issue_create | radieu/p24-infra#1553 | Tworzenie issue — opcjonalne, low-value w masowych workflowach |
branch_create | feat/1553-infra-ops-log | Tworzenie gałęzi — opcjonalne |
Wrapper — log_op()
Kontrakt bezpieczeństwa
detailNIGDY nie zawiera wartości sekretu — tylko długość, prefiks, wynik- Wrappers walidują
detailprzed wysłaniem: regex check nasb_secret_,sk-ant-,ghp_,eyJ→ zastępuje[REDACTED]+ warning na stdout SUPABASE_SERVICE_KEYczytany wyłącznie z env (os.environ), nigdy jako argument CLI- Timeout HTTP: 3 sekundy; failure → silent skip (log na stdout), nigdy nie blokuje głównej operacji
Python: scripts/lib/log_op.py
import os, re, requests
_SECRET_PATTERNS = re.compile(r'sb_secret_|sk-ant-|ghp_|eyJ[A-Za-z0-9]')
_URL = os.environ.get('SUPABASE_URL', '')
_KEY = os.environ.get('SUPABASE_SERVICE_KEY', '')
def log_op(actor, op_type, resource, result, target=None, detail=None,
session_id=None, issue_ref=None):
if not (_URL and _KEY):
return # silent skip — env not configured
if detail and _SECRET_PATTERNS.search(detail):
print(f"[log_op WARNING] detail contained secret pattern — redacted")
detail = '[REDACTED]'
try:
requests.post(
f'{_URL}/rest/v1/infra_operations',
headers={'apikey': _KEY, 'Authorization': f'Bearer {_KEY}',
'Content-Type': 'application/json', 'Prefer': 'return=minimal'},
json={'actor': actor, 'op_type': op_type, 'resource': resource,
'result': result, 'target': target, 'detail': detail,
'session_id': session_id, 'issue_ref': issue_ref},
timeout=3
)
except Exception as e:
print(f"[log_op WARNING] failed to write audit log: {e}")Bash: scripts/lib/log_op.sh
log_op() {
# Usage: log_op actor op_type resource result [target] [detail] [session_id] [issue_ref]
local actor="$1" op_type="$2" resource="$3" result="$4"
local target="${5:-}" detail="${6:-}" session_id="${7:-}" issue_ref="${8:-}"
[[ -z "${SUPABASE_URL:-}" || -z "${SUPABASE_SERVICE_KEY:-}" ]] && return 0
# Guard: redact known secret patterns in detail
if echo "$detail" | grep -qE 'sb_secret_|sk-ant-|ghp_|eyJ[A-Za-z0-9]'; then
echo "[log_op WARNING] detail contained secret pattern — redacted" >&2
detail='[REDACTED]'
fi
local payload
payload=$(printf '{"actor":"%s","op_type":"%s","resource":"%s","result":"%s","target":"%s","detail":"%s","session_id":"%s","issue_ref":"%s"}' \
"$actor" "$op_type" "$resource" "$result" "$target" "$detail" "$session_id" "$issue_ref")
curl -sf --max-time 3 \
-X POST "${SUPABASE_URL}/rest/v1/infra_operations" \
-H "apikey: ${SUPABASE_SERVICE_KEY}" \
-H "Authorization: Bearer ${SUPABASE_SERVICE_KEY}" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d "$payload" >/dev/null 2>&1 || echo "[log_op WARNING] failed to write audit log" >&2
}GH Actions: secrets-sync.yml
- name: Log deploy to infra_operations
continue-on-error: true # CRITICAL: nigdy nie blokuje deployu
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
run: |
curl -sf --max-time 3 \
-X POST "${SUPABASE_URL}/rest/v1/infra_operations" \
-H "apikey: ${SUPABASE_SERVICE_KEY}" \
-H "Authorization: Bearer ${SUPABASE_SERVICE_KEY}" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d "{\"actor\":\"gh-actions/secrets-sync\",\"op_type\":\"deploy\",\"resource\":\"$TARGET_ENV@$TARGET_HOST\",\"result\":\"success\",\"target\":\"$TARGET_HOST\"}" \
>/dev/null || echo "audit log skip"Zależności implementacji
PARALLEL:
[M1] Supabase migration — infra_operations + RLS + indeksy
[M2] scripts/lib/log_op.py
[M3] scripts/lib/log_op.sh
THEN (po M1+M2+M3):
[M4] Integracja: secrets-sync.yml (GH Actions, continue-on-error)
[M5] Integracja: credential-rotation playbooks (bash log_op)
[M6] Grafana dashboard "Infra Operations Audit"
[M7] dev_r_services INSERT + ops doc (infra_operations-operations.md)
THEN (po M1):
[M8] Retroaktywne wypełnienie z secrets-rotation-log.md (jednorazowy skrypt)
THEN (po M7):
[M9] audit-engine: action handler infra_ops_archive + INSERT do audit.actions
Numeracja migracji
Ostatnia migracja: 20260618170113_723_agent_sessions.sql
Nowa: 20260627_1553_infra_operations.sql (timestamp-based format)
Archiwizacja: 20260627_1553_infra_ops_archive_action.sql (audit.actions INSERT)
Checklist zgodności (przed zamknięciem PR)
-
ALTER TABLE infra_operations ENABLE ROW LEVEL SECURITYw migracji - Polityki
service_role_all+grafana_readonly_selectw migracji -
dev_r_servicesINSERT zcompliance_workbook='yes' -
infra_operations-operations.mdwdocs/ -
continue-on-error: truena kroku log wsecrets-sync.yml -
timeout=3w Python wrapper - Secret pattern validation w obu wrappers
- Wasabi prefix
infra-ops/istnieje (lub tworzony przez handler) -
audit.actionsINSERT dlainfra_ops_archive