// Package config provides configuration loading for SPIRE plugins. // SPIRE plugins receive configuration via HCL in the SPIRE server/agent config file. package config import ( "fmt" "strings" "github.com/hashicorp/hcl/v2/hclsimple" ) const ( // DefaultGovernanceEpochSeconds is used when governance_epoch_seconds is not set. DefaultGovernanceEpochSeconds = 300 // MinGovernanceEpochSeconds is the minimum allowed epoch duration. MinGovernanceEpochSeconds = 10 // MaxGovernanceEpochSeconds is the maximum allowed epoch duration. MaxGovernanceEpochSeconds = 3600 ) // PluginConfig holds common configuration fields shared by all Guildhouse SPIRE plugins. type PluginConfig struct { // GovernanceAddr is the gRPC address of the GovernanceService. GovernanceAddr string `hcl:"governance_addr"` // CeremonyAddr is the gRPC address of the CeremonyService. CeremonyAddr string `hcl:"ceremony_addr"` // NotaryAddr is the gRPC address of the NotaryService. NotaryAddr string `hcl:"notary_addr"` // TrustDomain is the SPIFFE trust domain. TrustDomain string `hcl:"trust_domain"` // ClusterID identifies this cluster for notary anchoring. ClusterID string `hcl:"cluster_id"` // GovernanceEpochSeconds is the duration of a governance epoch in seconds. // Controls how frequently merkle anchors are created. Must not exceed 256 // credential events per epoch (see shellstream merkle-proof depth limit). // Default: 300 (5 minutes). GovernanceEpochSeconds int `hcl:"governance_epoch_seconds"` } // Validate checks that required fields are present and within valid ranges. func (c *PluginConfig) Validate() error { if c.TrustDomain == "" { return fmt.Errorf("config: trust_domain is required") } if c.GovernanceAddr == "" { return fmt.Errorf("config: governance_addr is required") } if c.ClusterID == "" { return fmt.Errorf("config: cluster_id is required") } // Apply default and validate range for epoch seconds. if c.GovernanceEpochSeconds == 0 { c.GovernanceEpochSeconds = DefaultGovernanceEpochSeconds } if c.GovernanceEpochSeconds < MinGovernanceEpochSeconds { return fmt.Errorf("config: governance_epoch_seconds %d below minimum %d", c.GovernanceEpochSeconds, MinGovernanceEpochSeconds) } if c.GovernanceEpochSeconds > MaxGovernanceEpochSeconds { return fmt.Errorf("config: governance_epoch_seconds %d exceeds maximum %d", c.GovernanceEpochSeconds, MaxGovernanceEpochSeconds) } // CeremonyAddr and NotaryAddr are optional but must not be whitespace-only if set. if c.CeremonyAddr != "" && strings.TrimSpace(c.CeremonyAddr) == "" { return fmt.Errorf("config: ceremony_addr is set but empty") } if c.NotaryAddr != "" && strings.TrimSpace(c.NotaryAddr) == "" { return fmt.Errorf("config: notary_addr is set but empty") } return nil } // LoadFromHCL parses plugin configuration from HCL bytes. func LoadFromHCL(data []byte) (*PluginConfig, error) { cfg := &PluginConfig{} if err := hclsimple.Decode("plugin.hcl", data, nil, cfg); err != nil { return nil, fmt.Errorf("config: parse HCL: %w", err) } if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil }