Skip to content

Commit 68572bb

Browse files
committed
Unify the policy server config
use same config loading mechanism in the policy server as we use elsewhere
1 parent d2ce0a0 commit 68572bb

File tree

8 files changed

+327
-242
lines changed

8 files changed

+327
-242
lines changed

.tasks/closed/pxzg9n39.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
title: Unified policy server configuration
3+
id: pxzg9n39
4+
created: 2025-12-06T03:48:09.598197Z
5+
updated: 2025-12-06T03:52:11.061472Z
6+
author: Brian McCallister
7+
priority: high
8+
tags:
9+
- feature
10+
---
11+
12+
---
13+
## Log
14+
15+
---
16+
# Log: 2025-12-06T03:48:09Z Brian McCallister
17+
18+
Created task.
19+
---
20+
# Log: 2025-12-06T03:48:13Z Brian McCallister
21+
22+
Started working.
23+
---
24+
# Log: 2025-12-06T03:48:18Z Brian McCallister
25+
26+
Starting implementation - first updating main.go to bind CUE value
27+
---
28+
# Log: 2025-12-06T03:50:51Z Brian McCallister
29+
30+
Completed implementation:
31+
- Updated main.go to bind CUE value
32+
- Rewrote PolicyServerCLI with nested OIDC config
33+
- Renamed ca_public_key -> ca_pubkey in tags
34+
- Updated example configs
35+
- Updated tmp test files
36+
- All tests pass
37+
---
38+
# Log: 2025-12-06T03:52:11Z Brian McCallister
39+
40+
Closed: Completed unified policy server configuration

cmd/epithet/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func main() {
7272

7373
ktx.Bind(logger)
7474
ktx.Bind(tlsCfg)
75+
ktx.Bind(unifiedConfig) // Bind CUE value for commands that need full config (e.g., policy)
7576
err = ktx.Run()
7677
if err != nil {
7778
logger.Error("error", "error", err)

cmd/epithet/policy.go

Lines changed: 80 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
2632
type 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
90153
func 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

Comments
 (0)