@@ -7,14 +7,10 @@ import (
77 "log/slog"
88 "net/http"
99 "os"
10- "path/filepath"
1110 "strings"
1211 "time"
1312
1413 "cuelang.org/go/cue"
15- "cuelang.org/go/cue/cuecontext"
16- "cuelang.org/go/cue/load"
17- "cuelang.org/go/encoding/yaml"
1814 "github.com/epithet-ssh/epithet/pkg/policyserver"
1915 "github.com/epithet-ssh/epithet/pkg/policyserver/evaluator"
2016 "github.com/epithet-ssh/epithet/pkg/sshcert"
@@ -23,32 +19,54 @@ import (
2319 "github.com/go-chi/chi/v5/middleware"
2420)
2521
22+ // PolicyOIDCConfig holds OIDC configuration for the policy server.
23+ // This is embedded in PolicyServerCLI to enable nested config paths like policy.oidc.issuer
24+ type PolicyOIDCConfig struct {
25+ Issuer string `help:"OIDC issuer URL" name:"issuer"`
26+ Audience string `help:"OIDC audience (client ID)" name:"audience"`
27+ }
28+
29+ // PolicyServerCLI defines the CLI flags for the policy server.
30+ // Configuration comes from ~/.epithet/*.yaml under the "policy" section,
31+ // or from command-line flags. Maps (users, hosts) can only come from config files.
2632type PolicyServerCLI struct {
27- ConfigFile string `help:"Path to policy configuration file (YAML or CUE)" short:"c" required:"true"`
28- Listen string `help:"Address to listen on" short:"l" default:"0.0.0.0:9999"`
29- CAPubkey string `help:"CA public key (URL like http://localhost:8080, file path, or literal SSH key)" required:"true"`
33+ Listen string `help:"Address to listen on" short:"l" default:"0.0.0.0:9999"`
34+
35+ // Nested struct for OIDC - gives us policy.oidc.issuer in config
36+ OIDC PolicyOIDCConfig `embed:"" prefix:"oidc-"`
37+
38+ // CA public key - can be URL, file path, or literal key
39+ CAPubkey string `help:"CA public key (URL, file path, or literal SSH key)" name:"ca-pubkey"`
40+
41+ // Default expiration
42+ DefaultExpiration string `help:"Default certificate expiration (e.g., 5m)" name:"default-expiration"`
3043}
3144
32- func (c * PolicyServerCLI ) Run (logger * slog.Logger , tlsCfg tlsconfig.Config ) error {
33- // Resolve CA public key (may fetch from URL )
34- caPubkey , err := resolveCAPubkey ( c . CAPubkey , tlsCfg )
45+ func (c * PolicyServerCLI ) Run (logger * slog.Logger , tlsCfg tlsconfig.Config , unifiedConfig cue. Value ) error {
46+ // Load policy configuration from unified CUE config (handles maps like users, hosts )
47+ cfg , err := c . loadPolicyFromCUE ( unifiedConfig )
3548 if err != nil {
36- return err
49+ return fmt . Errorf ( "failed to load policy config: %w" , err )
3750 }
3851
39- // Load policy configuration
40- logger .Info ("loading policy configuration" , "file" , c .ConfigFile )
41- cfg , err := loadPolicyConfig (c .ConfigFile )
52+ // Apply CLI overrides (scalar values take precedence over config file)
53+ c .applyOverrides (cfg )
54+
55+ // Resolve CA public key (may fetch from URL)
56+ if cfg .CAPublicKey == "" {
57+ return fmt .Errorf ("ca_pubkey is required (via --ca-pubkey flag or policy.ca_pubkey in config)" )
58+ }
59+ caPubkey , err := resolveCAPubkey (cfg .CAPublicKey , tlsCfg )
4260 if err != nil {
43- return fmt . Errorf ( "failed to load policy config: %w" , err )
61+ return err
4462 }
63+ cfg .CAPublicKey = caPubkey
4564
4665 // Validate policy configuration
4766 if err := cfg .Validate (); err != nil {
4867 return fmt .Errorf ("invalid policy config: %w" , err )
4968 }
5069
51- // Validate configuration
5270 logger .Info ("policy configuration loaded" ,
5371 "users" , len (cfg .Users ),
5472 "hosts" , len (cfg .Hosts ),
@@ -80,12 +98,57 @@ func (c *PolicyServerCLI) Run(logger *slog.Logger, tlsCfg tlsconfig.Config) erro
8098
8199 logger .Info ("starting policy server" ,
82100 "listen" , c .Listen ,
83- "config_file" , c .ConfigFile ,
84101 "ca_pubkey_length" , len (caPubkey ))
85102
86103 return http .ListenAndServe (c .Listen , r )
87104}
88105
106+ // loadPolicyFromCUE decodes the policy section from the unified CUE config.
107+ // This handles maps (users, hosts) that cannot be represented as CLI flags.
108+ func (c * PolicyServerCLI ) loadPolicyFromCUE (unifiedConfig cue.Value ) (* policyserver.PolicyRulesConfig , error ) {
109+ // Look up policy section
110+ policyVal := unifiedConfig .LookupPath (cue .ParsePath ("policy" ))
111+ if ! policyVal .Exists () {
112+ // Return empty config - CLI flags will provide required values
113+ return & policyserver.PolicyRulesConfig {
114+ Users : make (map [string ][]string ),
115+ }, nil
116+ }
117+
118+ // Decode into PolicyRulesConfig (CUE handles maps correctly)
119+ var cfg policyserver.PolicyRulesConfig
120+ if err := policyVal .Decode (& cfg ); err != nil {
121+ return nil , fmt .Errorf ("failed to decode policy config: %w" , err )
122+ }
123+
124+ // Ensure Users map is not nil
125+ if cfg .Users == nil {
126+ cfg .Users = make (map [string ][]string )
127+ }
128+
129+ return & cfg , nil
130+ }
131+
132+ // applyOverrides applies CLI-provided values over config file values.
133+ // Only non-empty CLI values override the config.
134+ func (c * PolicyServerCLI ) applyOverrides (cfg * policyserver.PolicyRulesConfig ) {
135+ if c .CAPubkey != "" {
136+ cfg .CAPublicKey = c .CAPubkey
137+ }
138+ if c .OIDC .Issuer != "" {
139+ cfg .OIDC .Issuer = c .OIDC .Issuer
140+ }
141+ if c .OIDC .Audience != "" {
142+ cfg .OIDC .Audience = c .OIDC .Audience
143+ }
144+ if c .DefaultExpiration != "" {
145+ if cfg .Defaults == nil {
146+ cfg .Defaults = & policyserver.DefaultPolicy {}
147+ }
148+ cfg .Defaults .Expiration = c .DefaultExpiration
149+ }
150+ }
151+
89152// resolveCAPubkey resolves the CA public key from a URL, file path, or literal key
90153func resolveCAPubkey (input string , tlsCfg tlsconfig.Config ) (string , error ) {
91154 // Check if it's a URL
@@ -136,87 +199,3 @@ func resolveCAPubkey(input string, tlsCfg tlsconfig.Config) (string, error) {
136199
137200 return input , nil
138201}
139-
140- // loadPolicyConfig loads policy configuration from a file (YAML, JSON, or CUE).
141- func loadPolicyConfig (path string ) (* policyserver.PolicyRulesConfig , error ) {
142- ctx := cuecontext .New ()
143-
144- fileInfo , err := os .Stat (path )
145- if err != nil {
146- return nil , fmt .Errorf ("failed to stat path: %w" , err )
147- }
148-
149- var val cue.Value
150-
151- // Handle directories and .cue files using load.Instances
152- if fileInfo .IsDir () || strings .HasSuffix (strings .ToLower (path ), ".cue" ) {
153- absPath , err := filepath .Abs (path )
154- if err != nil {
155- return nil , fmt .Errorf ("failed to resolve path: %w" , err )
156- }
157-
158- cfg := & load.Config {
159- Dir : filepath .Dir (absPath ),
160- DataFiles : true ,
161- }
162-
163- var args []string
164- if fileInfo .IsDir () {
165- args = []string {path }
166- } else {
167- args = []string {absPath }
168- }
169-
170- instances := load .Instances (args , cfg )
171- if len (instances ) == 0 {
172- return nil , fmt .Errorf ("no instances loaded from %s" , path )
173- }
174-
175- inst := instances [0 ]
176- if inst .Err != nil {
177- return nil , fmt .Errorf ("failed to load config: %w" , inst .Err )
178- }
179-
180- val = ctx .BuildInstance (inst )
181- if err := val .Err (); err != nil {
182- return nil , fmt .Errorf ("failed to build CUE value: %w" , err )
183- }
184- } else {
185- // Handle standalone data files (YAML, JSON)
186- data , err := os .ReadFile (path )
187- if err != nil {
188- return nil , fmt .Errorf ("failed to read file: %w" , err )
189- }
190-
191- ext := strings .ToLower (filepath .Ext (path ))
192- switch ext {
193- case ".yaml" , ".yml" :
194- file , err := yaml .Extract ("" , data )
195- if err != nil {
196- return nil , fmt .Errorf ("failed to parse YAML: %w" , err )
197- }
198- val = ctx .BuildFile (file )
199- case ".json" :
200- val = ctx .CompileBytes (data )
201- default :
202- // Try YAML as default
203- file , err := yaml .Extract ("" , data )
204- if err != nil {
205- return nil , fmt .Errorf ("failed to parse file: %w" , err )
206- }
207- val = ctx .BuildFile (file )
208- }
209-
210- if err := val .Err (); err != nil {
211- return nil , fmt .Errorf ("failed to build CUE value: %w" , err )
212- }
213- }
214-
215- // Decode into PolicyRulesConfig
216- var config policyserver.PolicyRulesConfig
217- if err := val .Decode (& config ); err != nil {
218- return nil , fmt .Errorf ("failed to decode config: %w" , err )
219- }
220-
221- return & config , nil
222- }
0 commit comments