orama/core/pkg/gateway/secrets_key.go
anonpenguin23 34f9da6f8d feat(gateway): implement ntfy cluster fan-out and improve secrets encryption
- Add `ntfyFanoutResolver` to distribute push notifications across all active cluster nodes, ensuring delivery when nodes lack shared state.
- Refactor secrets encryption key derivation to use cluster-wide secrets via HKDF, replacing ephemeral per-node keys to fix cross-node decryption issues.
- Add unit tests for fan-out resolution logic and caching behavior.
2026-06-13 09:23:14 +03:00

50 lines
2.3 KiB
Go

package gateway
import (
"encoding/hex"
"strings"
"github.com/DeBrosOfficial/network/pkg/secrets"
)
// secretsEncryptionDerivePurpose is the HKDF info label used to derive the
// function-secrets AES-256 key from the cluster secret. Deriving it (instead of
// generating a per-node crypto/rand key file) guarantees every gateway in the
// cluster computes the IDENTICAL key, so a secret written on one node decrypts
// on every other node and survives rolling upgrades — eliminating the
// key-divergence / convergence-window class that kept get_secret broken for
// days (bugboard #837). Same pattern as the cluster-wide JWT signing key
// (jwtEdDSADerivePurpose) and the TURN encryption key ("turn-encryption").
//
// Bumping the version label (e.g. "...-v2") is a DELIBERATE rotation that
// invalidates every stored function secret (they must be re-`set`). It must
// never be changed casually.
const secretsEncryptionDerivePurpose = "orama-secrets-encryption-v1"
// resolveSecretsEncryptionKeyHex returns the hex-encoded AES-256 key the
// serverless secrets manager should use to encrypt/decrypt function secrets.
//
// Primary: derive deterministically from the cluster secret via HKDF, so the
// key is identical on every gateway in the cluster and stable across restarts
// and rolling upgrades. The cluster secret is TrimSpace'd first so a stray
// trailing newline on one node's secret file can't silently diverge its derived
// key from the rest of the cluster (the host gateway reads the file untrimmed
// while the namespace gateway trims it — without this they could derive
// different keys and reintroduce #837).
//
// Fallback: when no cluster secret is available (single-node test rigs / legacy
// deployments without a shared secret), fall back to an explicitly-configured
// key file. An empty result then makes the production secrets manager fail loud
// (NewDBSecretsManager with allowEphemeral=false), rather than silently using a
// per-process ephemeral key.
func resolveSecretsEncryptionKeyHex(clusterSecret, fileKeyHex string) (string, error) {
if cs := strings.TrimSpace(clusterSecret); cs != "" {
key, err := secrets.DeriveKey(cs, secretsEncryptionDerivePurpose)
if err != nil {
return "", err
}
return hex.EncodeToString(key), nil
}
return strings.TrimSpace(fileKeyHex), nil
}