Compare commits

...
Author SHA1 Message Date
cayce 797c37d312 docs: log de sesion 2026-11-01 (opencode) + propuesta de claves por agente/host
ci / lint-test-leaks (pull_request) Waiting to run
2026-11-01 20:35:16 +00:00
cayce 398d01fd4c Merge: regla permanente de secretos (anti-filtración)
ci / lint-test-leaks (push) Canceled after 0s
2026-10-31 18:00:41 +00:00
cayce 6ddf76983b regla permanente de secretos: test anti-patrones + hook de journald
ci / lint-test-leaks (pull_request) Canceled after 0s
Nueva regla del operador (2026-10-31): los ficheros de secretos solo se
inspeccionan con herramientas que no muestran valores (jq keys, grep -c,
stat). Prohibido cat/grep -r sobre .env y volcar errores con DSN.

- tests/test_no_credential_patterns.py: 3 tests — DSN con credencial
  literal fuera de placeholders, nsec bech32 versionado, tokens hex de
  64 chars fuera de tests/.
- scripts/check-credential-leaks.sh: hook que falla si journald
  reciente contiene DSN con contraseña o nsec (redactando el valor en
  la salida). Sin timer propio (no autorizado); ejecutar manual o desde
  cron del operador.

Contexto: la rotación del rol hardcap y el DSN filtrado a journald por
el test del gate (corregido; la credencial filtrada ya está rotada).
2026-10-31 18:00:26 +00:00
4 changed files with 139 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# Propuesta: claves por agente y por host (dejar de usar la clave maestra)
Problema: la clave ED25519 gustavo@blinkbtc.com (~/.ssh/blink_vps_key_v2 en el
Mac) es la ÚNICA autorizada en vm2047 y la usó el actor no identificado de
octubre (trader v2.4, burbujas, sfeed, respondedor). Un solo secreto, todo el
poder, cero trazabilidad por agente.
Propuesta (por orden de adopción):
1. Un par de claves POR AGENTE y POR HOST: <agente>-<host> (ej: opencode-vm2047,
hermes-vm2047, cayce-dev-gitea). ed25519, sin passphrase, en ~/.ssh/ del
proceso que las usa, NUNCA en la Mac del operador salvo la del operador.
2. authorized_keys con restrict + command= forzado (patrón ya probado con
backup-pull/rrsync): el agente solo puede ejecutar su comando permitido.
3. Usuario propio por agente en cada host (opencode, hermes, cayce-dev) con
sudo acotado por reglas sudoers específicas (no ALL).
4. Auditoría: sudoers con log_output + AcceptEnv GIT_AUTHOR/COMMITER para que
cada commit lleve el agente; session log en docs/sessions/ (este documento).
5. Rotación de salida: retirar gustavo@blinkbtc.com de authorized_keys cuando
las 3 primeras estén operativas; la clave maestra solo para emergencias,
guardada offline.
6. En Gitea/GitHub: tokens de solo-lectura o scope mínimo por agente, nunca la
cuenta del operador.
+36
View File
@@ -0,0 +1,36 @@
# Sesión 2026-11-01 — OpenCode/GLM en m1s-macbook-pro (operador: patternn)
Hosts: vm2047.lnvps.cloud (ssh lolvm), blink-vps (lectura), GitHub (gh, patternn), Gitea git.cayce-co.xyz.
## Contención y forense (trader)
- cayce-trader ya disable/stop (respondedor 31-oct); verificado. /etc/hosts: 16 endpoints Binance a 0.0.0.0, curl sin conexión.
- Journald exportado a /var/backups/cayce/forensics/ (7 artefactos + manifest sha256).
- Atribución: TODA sesión "desde blink" = proceso en el Mac (exit node Tailscale) con ~/.ssh/blink_vps_key_v2. v2.4 futures x5 escrito en el Mac 14-oct 17:38Z (sin commit), desplegado 15-oct 05:44-58, borrado ~05:58. Harness no identificado. Informes: FORENSICS-REPORT-20261101.md, ACTIONS-20261101.md.
## Infra
- Gitea TLS https://git.cayce-co.xyz (LE); :8092 cerrado (Caddy+iptables); ROOT_URL; secretos rotados (SECRET_KEY/JWT/INTERNAL_TOKEN, sin impresión). Usuario pattern (merge exclusivo) + script gitea-set-merge-owner.
- Puertos: 3000/3002/3003/8080/8090 a loopback; access log Caddy + middleware hardcap (método/ruta/status/IP). 8080 era host-matcher malentendido, no bind.
## Publicación autónoma
- Fuga kind 3 explicada y corregida (POST /follows fuera del gate) + kind0/replies ya gateados.
- guard.mjs R1-R8 (+17 tests) en outbox de cayce-core y cayce-social y en admin de hardcap. Rechazos → guard_rejections.
- Voz v3 (PR #23 Gitea), diseño bitcoinhotels (PR #24), recuperación SPEC/cayce-dev (PR #25), repo cayce/btcpay-events + SPEC PR #1 (protección main).
- Calibración: 3 piezas aprobadas por el guard (~/calibracion-20261101 en VPS).
- ACTIVADO live 2026-11-01 20:19 UTC (4 .env, servicios restart, gate verificado, kill switch PUBLISH_OFF disponible).
## Backups
- age recipient nuevo (Mac ~/.config/cayce-backup/age.key, secreto jamás impreso) + clave backup-pull instalada con rrsync -ro. Bugs: fallback PGURL, GRANT posts_log, NoNewPrivileges. Primer backup OK (3 .age, 1.6M) y pull del Mac verificado (manifest 3xOK). Cadena: VPS 03:15 UTC / Mac 23:30 + launchd.
## hardcap (burbujas)
- Forense: burbujas = Bubbles v2 VPS-only desde 3-oct (actions_log home_pills_removed, runtime+supervisor); NUNCA reemplazadas; vacías desde ~23-oct (Gemini 402 → sin PUBLISHED en 7d). 402 exacto: prepayment credits depleted (AI Studio), key válida.
- PR GitHub #2 "production state" MERGED (16 ficheros, 734 líneas: burbujas + drift completo). PR #1 (treemap, premisa errónea) CLOSED.
- /srv/hardcap-news = checkout git único para FE+BE (swap con rollback .pre-git-20261101; health 200; working tree limpio). Timer cayce-check-clean-trees diario 09:10 UTC.
- scripts/reprocess-pending.ts añadido (para cuando vuelvan créditos AI Studio).
## IG
- META_PAGE_TOKEN del .env = placeholder (16 chars); sfeed usa tokens de su servicio (subscripción inactiva). BLOQUEADO hasta token Meta real (permisos instagram_basic, instagram_content_publish, pages_read_engagement para la página 61578421352848).
## Pendientes del operador
- Recargar créditos AI Studio (aistudio.google.com → Billing) y correr reprocess.
- Token Meta para IG. Rotar tokens automation/automation2 de Gitea.
- Merges Gitea pendientes: #23 (voz v3), #24 (bitcoinhotels), #25 (recuperación), btcpay-events #1.
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# check-credential-leaks — falla si journald reciente contiene patrones de credencial.
# Regla permanente: los secretos jamás en logs. Ejecutar manualmente o desde cron
# (sin timer propio: no fue autorizado).
set -euo pipefail
SINCE="${1:-2 hours ago}"
HITS=$(journalctl --since "$SINCE" --no-pager 2>/dev/null | grep -cE "(postgres|postgresql)://[A-Za-z0-9_]+:[^@[:space:]]{8,}@|nsec1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,}" || true)
if [ "$HITS" -gt 0 ]; then
echo "ALERTA: $HITS líneas de journald (desde $SINCE) contienen patrones de credencial" >&2
journalctl --since "$SINCE" --no-pager 2>/dev/null | grep -nE "(postgres|postgresql)://[A-Za-z0-9_]+:[^@[:space:]]{8,}@|nsec1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,}" | sed -E "s#(://[A-Za-z0-9_]+:)[^@[:space:]]{8,}@#\1***@#" | head -5
exit 1
fi
echo "OK: sin patrones de credencial en journald desde $SINCE"
+68
View File
@@ -0,0 +1,68 @@
"""Regla permanente (2026-10-31): ningún fichero versionado contiene patrones
de credenciales. Placeholders de .env.example están permitidos.
Fallará si alguien mete un DSN con contraseña real, un token en claro, o
cualquier valor que deba vivir solo en .env / tokens.json.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
SCAN_DIRS = ["runtime", "deploy", "docs", "scripts", "prompts"]
SCAN_GLOBS = ["*.py", "*.mjs", "*.js", "*.ts", "*.md", "*.yml", "*.yaml", "*.sh", "*.sql"]
# DSN con credencial literal (user:pass@host) — excluye placeholders conocidos
DSN = re.compile(r"(postgres|postgresql|mysql|mongodb)(\+[a-z]+)?://[A-Za-z0-9_]+:(?!password\b|change-me\b|<[^>]+>)[^@\s/]{8,}@", re.I)
# tokens hex largos fuera de tests (los fixtures de tests están allowlistados por diseño)
HEXTOKEN = re.compile(r"\b[0-9a-f]{64}\b")
# nsec bech32
NSEC = re.compile(r"nsec1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,}")
ALLOW_SUBSTR = ("user:password", "change-me", "<", "EXAMPLE", "example", "itest-pg", "0011", "11*", "22*", "33*", "44*")
def _iter_files():
for d in SCAN_DIRS:
base = REPO / d
if not base.exists():
continue
for pat in SCAN_GLOBS:
for f in base.rglob(pat):
if ".git" in f.parts or "node_modules" in f.parts:
continue
yield f
def test_no_dsn_with_credentials():
hits = []
for f in _iter_files():
for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1):
if DSN.search(line) and not any(a in line for a in ALLOW_SUBSTR):
hits.append(f"{f}:{i}")
assert not hits, f"DSN con credencial literal en: {hits}"
def test_no_nsec_in_versioned_files():
hits = []
for f in _iter_files():
if "/tests/" in str(f):
continue
for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1):
if NSEC.search(line):
hits.append(f"{f}:{i}")
assert not hits, f"nsec en fichero versionado: {hits}"
def test_no_raw_hex_tokens_outside_tests():
"""Tokens hex de 64 chars fuera de tests/ y del conftest (las claves de
test viven solo en tests/). El outbox no debe contener secretos."""
hits = []
for f in _iter_files():
if "/tests/" in str(f) or f.name == "cayce-identity.md":
continue
for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1):
if HEXTOKEN.search(line):
hits.append(f"{f}:{i}")
assert not hits, f"posible token hex versionado: {hits}"