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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user