Files
ARTEX/server/env_scrub.go
T
BaaRai 07cc87c1a3 Encrypt stored secrets at rest in PostgreSQL
Seal the four credential columns with AES-256-GCM (random nonce per value,
AAD "<table>.<column>"): llm_profiles.api_key and task_credentials.password
store "enc:v1:<base64(nonce||ciphertext)>"; mcp_servers.env and
notification_channels.config store {"__artex_enc":"enc:v1:..."} over the
canonical JSON. The db layer encrypts on write and decrypts on read, so agent
tools (LLM provider, MCP, notifier, test credentials) get plaintext
server-side. Decryption failure is an error, never a plaintext fallback.

Key: ARTEX_SECRET_KEY (32 bytes, base64 or hex) or keys/secret.key next to
jwt.key, generated with crypto/rand at first start (0600). Startup refuses
when encrypted values exist and no key is available, and when the key does
not decrypt them. An idempotent backfill under the schema advisory lock
encrypts legacy plaintext rows and logs only a count.

API: MCP env is masked on read and masked placeholders on write keep the
stored value; task credential passwords are masked server-side and a masked
password on write is refused. ARTEX_SECRET_KEY is scrubbed from the process
environment with the other startup secrets.
2026-11-13 14:42:56 +01:00

31 lines
1.3 KiB
Go

package server
import "os"
// secretEnv lists the credentials read from the environment only at startup:
// ARTEX_PG_DSN and ARTEX_SECRET_KEY by NewManager, the API keys by agent.FromEnv inside New.
var secretEnv = []string{"ARTEX_PG_DSN", "ARTEX_SECRET_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"}
// ScrubSecretEnv removes secretEnv from the process environment once startup has
// read it. Child processes (agent shells, custom tools, MCP stdio servers) copy
// os.Environ() when they are spawned, so they no longer inherit these values.
//
// It runs inside New, after both readers and before the MCP discovery goroutine
// and the task resume loop: both can spawn children, and resuming a task starts
// workers that run shells. Scrubbing in main after New returned left that window
// open.
func ScrubSecretEnv() {
for _, k := range secretEnv {
_ = os.Unsetenv(k)
}
}
// Option configures New. Options keep the test call sites unchanged: only the
// binary opts into scrubbing, because tests create several servers in one
// process and still need ARTEX_PG_DSN on later calls.
type Option func(*Server)
// WithSecretScrub makes New drop the startup secrets after reading them, before
// the MCP discovery goroutine and the task resume loop can spawn children.
func WithSecretScrub() Option { return func(s *Server) { s.scrubSecrets = true } }