feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed migrations/wg_revoked/001_revoked_peers.sql
|
||||
var wgRevokedMigration string
|
||||
|
||||
// WGPeerRevokeAudit contiene el registro inmutable de una revocacion.
|
||||
type WGPeerRevokeAudit struct {
|
||||
DeviceID string
|
||||
PublicKey string
|
||||
RevokedAt int64
|
||||
RevokedBy string
|
||||
Reason string
|
||||
PrevHash string
|
||||
ThisHash string
|
||||
}
|
||||
|
||||
// WGPeerRevoke revoca permanentemente un peer: lo elimina del config activo,
|
||||
// lo registra en una audit DB con hash chain SHA256 inviolable y escribe en
|
||||
// la blacklist persistente /etc/wireguard/wg_revoked.list.
|
||||
//
|
||||
// Reglas:
|
||||
// - reason no puede estar vacio.
|
||||
// - Revocar un peer ya revocado devuelve error "already revoked".
|
||||
// - auditDBPath se crea con migracion embebida si no existe.
|
||||
// - this_hash = SHA256(prev_hash || device_id || public_key || revoked_at || revoked_by || reason)
|
||||
func WGPeerRevoke(deviceID, operator, reason string, configPath, auditDBPath string) (WGPeerRevokeAudit, error) {
|
||||
audit := WGPeerRevokeAudit{
|
||||
DeviceID: deviceID,
|
||||
RevokedBy: operator,
|
||||
Reason: reason,
|
||||
}
|
||||
|
||||
if strings.TrimSpace(deviceID) == "" {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: deviceID cannot be empty")
|
||||
}
|
||||
if strings.TrimSpace(operator) == "" {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: operator cannot be empty")
|
||||
}
|
||||
if strings.TrimSpace(reason) == "" {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: reason cannot be empty")
|
||||
}
|
||||
if strings.TrimSpace(configPath) == "" {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: configPath cannot be empty")
|
||||
}
|
||||
if strings.TrimSpace(auditDBPath) == "" {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: auditDBPath cannot be empty")
|
||||
}
|
||||
|
||||
// 1. Abrir/crear audit DB y aplicar migracion.
|
||||
if err := os.MkdirAll(filepath.Dir(auditDBPath), 0700); err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: mkdir audit db dir: %w", err)
|
||||
}
|
||||
db, err := sql.Open("sqlite3", auditDBPath+"?_journal_mode=WAL&_foreign_keys=on")
|
||||
if err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: open audit db: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if _, err := db.Exec(wgRevokedMigration); err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: apply migration: %w", err)
|
||||
}
|
||||
|
||||
// 2. Verificar que no este ya revocado ANTES del lookup (el peer puede
|
||||
// haber sido eliminado del config por una revocacion previa).
|
||||
var count int
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM revoked_peers WHERE device_id = ?", deviceID).Scan(&count); err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: check existing: %w", err)
|
||||
}
|
||||
if count > 0 {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: device %s already revoked", deviceID)
|
||||
}
|
||||
|
||||
// 3. Lookup PublicKey (el peer debe estar aun en el config en este punto).
|
||||
pubKey, err := wgLookupPeerPublicKey(deviceID, configPath)
|
||||
if err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: lookup peer: %w", err)
|
||||
}
|
||||
audit.PublicKey = pubKey
|
||||
|
||||
// 4. Obtener prev_hash (hash del ultimo registro, o string vacio si genesis).
|
||||
var prevHash string
|
||||
_ = db.QueryRow("SELECT this_hash FROM revoked_peers ORDER BY revoked_at DESC LIMIT 1").Scan(&prevHash)
|
||||
|
||||
// 5. Calcular this_hash = SHA256(prevHash || deviceID || publicKey || revokedAt || operator || reason).
|
||||
revokedAt := time.Now().Unix()
|
||||
audit.RevokedAt = revokedAt
|
||||
audit.PrevHash = prevHash
|
||||
|
||||
hashInput := fmt.Sprintf("%s|%s|%s|%d|%s|%s", prevHash, deviceID, pubKey, revokedAt, operator, reason)
|
||||
thisHash := fmt.Sprintf("%x", sha256.Sum256([]byte(hashInput)))
|
||||
audit.ThisHash = thisHash
|
||||
|
||||
// 6. WGPeerRemove (eliminar del config + syncconf).
|
||||
removeResult, err := WGPeerRemove(deviceID, configPath)
|
||||
if err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: remove peer: %w", err)
|
||||
}
|
||||
_ = removeResult // status puede ser removed o not-present (ya borrado, igual revocamos)
|
||||
|
||||
// 7. Insertar en audit DB dentro de una transaccion.
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: begin tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO revoked_peers (device_id, public_key, revoked_at, revoked_by, reason, prev_hash, this_hash)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
deviceID, pubKey, revokedAt, operator, reason, prevHash, thisHash,
|
||||
)
|
||||
if err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: insert audit record: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return audit, fmt.Errorf("wg_peer_revoke: commit audit record: %w", err)
|
||||
}
|
||||
|
||||
// 8. Append a blacklist persistente /etc/wireguard/wg_revoked.list.
|
||||
blacklistLine := fmt.Sprintf("%s %d # DeviceID:%s operator:%s reason:%s\n",
|
||||
pubKey, revokedAt, deviceID, operator, reason)
|
||||
if err := wgAppendBlacklistFn(blacklistLine); err != nil {
|
||||
// No revertir — el audit DB ya tiene el registro. Loguear como advertencia.
|
||||
return audit, fmt.Errorf("wg_peer_revoke: append blacklist (audit DB committed): %w", err)
|
||||
}
|
||||
|
||||
return audit, nil
|
||||
}
|
||||
|
||||
// wgAppendBlacklistFn escribe una linea al final de /etc/wireguard/wg_revoked.list (append-only).
|
||||
// Variable para permitir override en tests sin requerir permisos de /etc/wireguard/.
|
||||
var wgAppendBlacklistFn = func(line string) error {
|
||||
const blacklistPath = "/etc/wireguard/wg_revoked.list"
|
||||
f, err := os.OpenFile(blacklistPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open blacklist %s: %w", blacklistPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.WriteString(line); err != nil {
|
||||
return fmt.Errorf("write blacklist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user