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

#DecyzjaWynik
D1detail formattext — prosty, bezpieczny
D3GH ActionsTak — secrets-sync.yml loguje deploy operacje
D5Lokalne sesjeOpcjonalne — log_op() silent-skip gdy brak env
D6RetroaktywneOd rotacji 2026-06-26/27 (secrets-rotation-log.md)
D7GrafanaNowy dedykowany dashboard “Infra Operations Audit”
D-newsession_start/endTylko dla nie-agentowych procesów (GH Actions, cron)
D-retentionRetencjaSupabase: 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/end tutaj — mają dev_r_agent_sessions.

op_typeresource przykładKiedy logować
session_startgh-actions/secrets-syncStart GH Actions job lub cron skryptu
session_endgh-actions/secrets-syncKoniec jobu (result: success/failed)

Admin (sysadmin)

op_typeresource przykładKiedy logować
credential_rotationSUPABASE_SERVICE_ROLE_KEYRotacja każdego credentialu
sops_editsops:monitoring.env.sopsEdycja i re-encrypt pliku SOPS
ssh_sessionroot@vps-i1Otwarcie sesji SSH
firewall_changeufw@bms-2Zmiana reguły ufw/iptables
user_managementclaude-runner@bms-4Tworzenie/usuwanie użytkowników OS/DB
ssl_cert_renewal*.vps-i1.infra.zintegrowana.onlineOdnowienie certyfikatu
backup_restorep24-infra/thanos@wasabiPrzywrócenie backupu
server_provisionvps-i2Provisioning nowego serwera
dns_changegrafana.vps-i1.infra.zintegrowana.onlineZmiana rekordu DNS

DevOps

op_typeresource przykładKiedy logować
service_restartmonitoring-grafana-1Restart kontenera/serwisu
service_startmonitoring-prometheus-1Start serwisu
service_stopmonitoring-caddy-1Zatrzymanie serwisu
deploymonitoring/.env@vps-i1Deployment aktualizacji
config_changemonitoring/prometheus.ymlZmiana konfiguracji
docker_pullgrafana/grafana:latestPull nowego obrazu
container_cleanupdocker-prune@vps-i1Sprzątanie kontenerów/obrazów
alert_silencealertmanager@vps-i1Wyciszenie alertu
incident_responsemonitoring-stack@vps-i1Ręczna interwencja

Developer

op_typeresource przykładKiedy logować
migrationsupabase:027_dev_r_agent_sessionsMigracja Supabase/DB
gh_pr_createradieu/p24-infra#1560Tworzenie PR
gh_pr_mergeradieu/p24-infra#1560Merge PR
code_deploymonitoring-exporters@vps-i1Deployment kodu przez CI
gh_issue_createradieu/p24-infra#1553Tworzenie issue — opcjonalne, low-value w masowych workflowach
branch_createfeat/1553-infra-ops-logTworzenie gałęzi — opcjonalne

Wrapper — log_op()

Kontrakt bezpieczeństwa

  • detail NIGDY nie zawiera wartości sekretu — tylko długość, prefiks, wynik
  • Wrappers walidują detail przed wysłaniem: regex check na sb_secret_, sk-ant-, ghp_, eyJ → zastępuje [REDACTED] + warning na stdout
  • SUPABASE_SERVICE_KEY czytany 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 SECURITY w migracji
  • Polityki service_role_all + grafana_readonly_select w migracji
  • dev_r_services INSERT z compliance_workbook='yes'
  • infra_operations-operations.md w docs/
  • continue-on-error: true na kroku log w secrets-sync.yml
  • timeout=3 w Python wrapper
  • Secret pattern validation w obu wrappers
  • Wasabi prefix infra-ops/ istnieje (lub tworzony przez handler)
  • audit.actions INSERT dla infra_ops_archive