Phase C: Automated TLS cert provisioning for L7 routes

Add CertProvisioner that requests TLS certificates from Metacrypt's CA
API during deploy. When a service has L7 routes, the agent checks for
an existing cert, re-issues if missing or within 30 days of expiry,
and writes chain+key to mc-proxy's cert directory before registering
routes.

- Add MetacryptConfig to agent config (server_url, ca_cert, mount,
  issuer, token_path) with defaults and env overrides
- Add CertProvisioner (internal/agent/certs.go): REST client for
  Metacrypt IssueCert, atomic file writes, cert expiry checking
- Wire into Agent struct and deploy flow (before route registration)
- Add hasL7Routes/l7Hostnames helpers in deploy.go
- Fix pre-existing lint issues: unreachable code in portalloc.go,
  gofmt in servicedef.go, gosec suppressions, golangci v2 config
- Update vendored mc-proxy to fix protobuf init panic
- 10 new tests, make all passes with 0 issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 13:31:11 -07:00
parent 572d2fb196
commit c7e1232f98
13 changed files with 832 additions and 26 deletions

View File

@@ -10,13 +10,34 @@ import (
// AgentConfig is the configuration for the mcp-agent daemon.
type AgentConfig struct {
Server ServerConfig `toml:"server"`
Database DatabaseConfig `toml:"database"`
MCIAS MCIASConfig `toml:"mcias"`
Agent AgentSettings `toml:"agent"`
MCProxy MCProxyConfig `toml:"mcproxy"`
Monitor MonitorConfig `toml:"monitor"`
Log LogConfig `toml:"log"`
Server ServerConfig `toml:"server"`
Database DatabaseConfig `toml:"database"`
MCIAS MCIASConfig `toml:"mcias"`
Agent AgentSettings `toml:"agent"`
MCProxy MCProxyConfig `toml:"mcproxy"`
Metacrypt MetacryptConfig `toml:"metacrypt"`
Monitor MonitorConfig `toml:"monitor"`
Log LogConfig `toml:"log"`
}
// MetacryptConfig holds the Metacrypt CA integration settings for
// automated TLS cert provisioning. If ServerURL is empty, cert
// provisioning is disabled.
type MetacryptConfig struct {
// ServerURL is the Metacrypt API base URL (e.g. "https://metacrypt:8443").
ServerURL string `toml:"server_url"`
// CACert is the path to the CA certificate for verifying Metacrypt's TLS.
CACert string `toml:"ca_cert"`
// Mount is the CA engine mount name. Defaults to "pki".
Mount string `toml:"mount"`
// Issuer is the intermediate CA issuer name. Defaults to "infra".
Issuer string `toml:"issuer"`
// TokenPath is the path to the MCIAS service token file.
TokenPath string `toml:"token_path"`
}
// MCProxyConfig holds the mc-proxy connection settings.
@@ -150,6 +171,12 @@ func applyAgentDefaults(cfg *AgentConfig) {
if cfg.MCProxy.CertDir == "" {
cfg.MCProxy.CertDir = "/srv/mc-proxy/certs"
}
if cfg.Metacrypt.Mount == "" {
cfg.Metacrypt.Mount = "pki"
}
if cfg.Metacrypt.Issuer == "" {
cfg.Metacrypt.Issuer = "infra"
}
}
func applyAgentEnvOverrides(cfg *AgentConfig) {
@@ -180,6 +207,12 @@ func applyAgentEnvOverrides(cfg *AgentConfig) {
if v := os.Getenv("MCP_AGENT_MCPROXY_CERT_DIR"); v != "" {
cfg.MCProxy.CertDir = v
}
if v := os.Getenv("MCP_AGENT_METACRYPT_SERVER_URL"); v != "" {
cfg.Metacrypt.ServerURL = v
}
if v := os.Getenv("MCP_AGENT_METACRYPT_TOKEN_PATH"); v != "" {
cfg.Metacrypt.TokenPath = v
}
}
func validateAgentConfig(cfg *AgentConfig) error {

View File

@@ -163,6 +163,14 @@ func TestLoadAgentConfig(t *testing.T) {
if cfg.Log.Level != "debug" {
t.Fatalf("log.level: got %q", cfg.Log.Level)
}
// Metacrypt defaults when section is omitted.
if cfg.Metacrypt.Mount != "pki" {
t.Fatalf("metacrypt.mount default: got %q, want pki", cfg.Metacrypt.Mount)
}
if cfg.Metacrypt.Issuer != "infra" {
t.Fatalf("metacrypt.issuer default: got %q, want infra", cfg.Metacrypt.Issuer)
}
}
func TestCLIConfigValidation(t *testing.T) {
@@ -439,6 +447,80 @@ level = "info"
})
}
func TestAgentConfigMetacrypt(t *testing.T) {
cfgStr := `
[server]
grpc_addr = "0.0.0.0:9444"
tls_cert = "/srv/mcp/cert.pem"
tls_key = "/srv/mcp/key.pem"
[database]
path = "/srv/mcp/mcp.db"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp-agent"
[agent]
node_name = "rift"
[metacrypt]
server_url = "https://metacrypt.metacircular.net:8443"
ca_cert = "/etc/mcp/metacircular-ca.pem"
mount = "custom-pki"
issuer = "custom-issuer"
token_path = "/srv/mcp/metacrypt-token"
`
path := writeTempConfig(t, cfgStr)
cfg, err := LoadAgentConfig(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.Metacrypt.ServerURL != "https://metacrypt.metacircular.net:8443" {
t.Fatalf("metacrypt.server_url: got %q", cfg.Metacrypt.ServerURL)
}
if cfg.Metacrypt.CACert != "/etc/mcp/metacircular-ca.pem" {
t.Fatalf("metacrypt.ca_cert: got %q", cfg.Metacrypt.CACert)
}
if cfg.Metacrypt.Mount != "custom-pki" {
t.Fatalf("metacrypt.mount: got %q", cfg.Metacrypt.Mount)
}
if cfg.Metacrypt.Issuer != "custom-issuer" {
t.Fatalf("metacrypt.issuer: got %q", cfg.Metacrypt.Issuer)
}
if cfg.Metacrypt.TokenPath != "/srv/mcp/metacrypt-token" {
t.Fatalf("metacrypt.token_path: got %q", cfg.Metacrypt.TokenPath)
}
}
func TestAgentConfigMetacryptEnvOverrides(t *testing.T) {
minimal := `
[server]
grpc_addr = "0.0.0.0:9444"
tls_cert = "/srv/mcp/cert.pem"
tls_key = "/srv/mcp/key.pem"
[database]
path = "/srv/mcp/mcp.db"
[mcias]
server_url = "https://mcias.metacircular.net:8443"
service_name = "mcp-agent"
[agent]
node_name = "rift"
`
t.Setenv("MCP_AGENT_METACRYPT_SERVER_URL", "https://override.metacrypt:8443")
t.Setenv("MCP_AGENT_METACRYPT_TOKEN_PATH", "/override/token")
path := writeTempConfig(t, minimal)
cfg, err := LoadAgentConfig(path)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.Metacrypt.ServerURL != "https://override.metacrypt:8443" {
t.Fatalf("metacrypt.server_url: got %q", cfg.Metacrypt.ServerURL)
}
if cfg.Metacrypt.TokenPath != "/override/token" {
t.Fatalf("metacrypt.token_path: got %q", cfg.Metacrypt.TokenPath)
}
}
func TestDurationParsing(t *testing.T) {
tests := []struct {
input string