-
Notifications
You must be signed in to change notification settings - Fork 0
Auto cert provisioning #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e982bf5
06e3fc1
af9e5d4
37c54a1
e36932f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "crypto/tls" | ||
| "encoding/json" | ||
| "flag" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "github.com/EternisAI/silo-proxy/internal/api/http/dto" | ||
| ) | ||
|
|
||
| func runProvision(args []string) error { | ||
| fs := flag.NewFlagSet("provision", flag.ExitOnError) | ||
| server := fs.String("server", "", "Server URL (e.g., https://server:8080)") | ||
| key := fs.String("key", "", "Provision key") | ||
| certDir := fs.String("cert-dir", "./certs", "Directory to save certificates") | ||
| insecure := fs.Bool("insecure", false, "Skip TLS certificate verification (for development only)") | ||
| if err := fs.Parse(args); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if *server == "" { | ||
| return fmt.Errorf("--server is required") | ||
| } | ||
| if *key == "" { | ||
| return fmt.Errorf("--key is required") | ||
| } | ||
|
|
||
| if *insecure { | ||
| fmt.Fprintln(os.Stderr, "WARNING: Using insecure TLS mode. This is unsafe for production.") | ||
| } | ||
|
|
||
| reqBody, err := json.Marshal(dto.ProvisionRequest{Key: *key}) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to marshal request: %w", err) | ||
| } | ||
|
|
||
| client := &http.Client{ | ||
| Transport: &http.Transport{ | ||
| TLSClientConfig: &tls.Config{ | ||
| InsecureSkipVerify: *insecure, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| url := *server + "/api/v1/provision" | ||
| resp, err := client.Post(url, "application/json", bytes.NewBuffer(reqBody)) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to connect to server: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to read response: %w", err) | ||
| } | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return fmt.Errorf("provisioning failed (HTTP %d): %s", resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| var provResp dto.ProvisionResponse | ||
| if err := json.Unmarshal(body, &provResp); err != nil { | ||
| return fmt.Errorf("failed to parse response: %w", err) | ||
| } | ||
|
|
||
| agentCertDir := filepath.Join(*certDir, "agents", provResp.AgentID) | ||
| caCertDir := filepath.Join(*certDir, "ca") | ||
|
|
||
| if err := os.MkdirAll(agentCertDir, 0700); err != nil { | ||
| return fmt.Errorf("failed to create directory %s: %w", agentCertDir, err) | ||
| } | ||
| if err := os.MkdirAll(caCertDir, 0755); err != nil { | ||
| return fmt.Errorf("failed to create directory %s: %w", caCertDir, err) | ||
| } | ||
|
|
||
| certPath := filepath.Join(agentCertDir, provResp.AgentID+"-cert.pem") | ||
| keyPath := filepath.Join(agentCertDir, provResp.AgentID+"-key.pem") | ||
| caPath := filepath.Join(caCertDir, "ca-cert.pem") | ||
|
|
||
| if err := os.WriteFile(certPath, []byte(provResp.CertPEM), 0644); err != nil { | ||
| return fmt.Errorf("failed to write cert: %w", err) | ||
| } | ||
| if err := os.WriteFile(keyPath, []byte(provResp.KeyPEM), 0600); err != nil { | ||
| return fmt.Errorf("failed to write key: %w", err) | ||
| } | ||
| if err := os.WriteFile(caPath, []byte(provResp.CACertPEM), 0644); err != nil { | ||
| return fmt.Errorf("failed to write CA cert: %w", err) | ||
| } | ||
|
|
||
| fmt.Println("Provisioning successful!") | ||
| fmt.Printf(" Agent ID: %s\n", provResp.AgentID) | ||
| fmt.Printf(" Cert: %s\n", certPath) | ||
| fmt.Printf(" Key: %s\n", keyPath) | ||
| fmt.Printf(" CA Cert: %s\n", caPath) | ||
| fmt.Println() | ||
| fmt.Println("Add the following to your agent application.yaml:") | ||
| fmt.Println() | ||
| fmt.Printf("grpc:\n") | ||
| fmt.Printf(" agent_id: \"%s\"\n", provResp.AgentID) | ||
| fmt.Printf(" tls:\n") | ||
| fmt.Printf(" enabled: true\n") | ||
| fmt.Printf(" cert_file: %s\n", certPath) | ||
| fmt.Printf(" key_file: %s\n", keyPath) | ||
| fmt.Printf(" ca_file: %s\n", caPath) | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,6 +17,7 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/EternisAI/silo-proxy/internal/db" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/EternisAI/silo-proxy/internal/db/sqlc" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| grpcserver "github.com/EternisAI/silo-proxy/internal/grpc/server" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/EternisAI/silo-proxy/internal/provision" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/EternisAI/silo-proxy/internal/users" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/gin-contrib/cors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/gin-gonic/gin" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -92,11 +93,25 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "range_end", config.Http.AgentPortRange.End, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "pool_size", config.Http.AgentPortRange.End-config.Http.AgentPortRange.Start+1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var keyStore *provision.KeyStore | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if config.Provision.Enabled { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if certService == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slog.Error("Provisioning requires TLS to be enabled") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ttl := time.Duration(config.Provision.KeyTTLHours) * time.Hour | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keyStore = provision.NewKeyStore(ttl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanupInterval := time.Duration(config.Provision.CleanupIntervalMinutes) * time.Minute | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| go keyStore.StartCleanup(context.Background(), cleanupInterval) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slog.Info("Provisioning enabled", "key_ttl_hours", config.Provision.KeyTTLHours) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+96
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find the provision package and StartCleanup function
find . -path ./vendor -prune -o -type f -name "*.go" -print | xargs grep -l "func.*StartCleanup" | head -5Repository: EternisAI/silo-proxy Length of output: 97 🏁 Script executed: #!/bin/bash
# Search for StartCleanup implementation across the codebase
rg -n "func.*StartCleanup" -A 10 --type goRepository: EternisAI/silo-proxy Length of output: 685 🏁 Script executed: #!/bin/bash
# Check the config structure for Provision settings
rg -n "type.*Provision|KeyTTLHours|CleanupIntervalMinutes" --type go | head -20Repository: EternisAI/silo-proxy Length of output: 1245 🏁 Script executed: #!/bin/bash
# Check NewKeyStore implementation and how it handles TTL
rg -n "func NewKeyStore" -A 10 --type goRepository: EternisAI/silo-proxy Length of output: 773 🏁 Script executed: #!/bin/bash
# Check config loading and validation
rg -n "config.*Provision|viper|unmarshal" --type go | grep -i "provision\|config" | head -20Repository: EternisAI/silo-proxy Length of output: 467 🏁 Script executed: #!/bin/bash
# Search for any existing validation of these config values
rg -n "KeyTTLHours|CleanupIntervalMinutes" --type goRepository: EternisAI/silo-proxy Length of output: 604 🏁 Script executed: #!/bin/bash
# Check time.NewTicker behavior with documentation or test references
rg -n "time.NewTicker|StartCleanup" -B 2 -A 5 --type goRepository: EternisAI/silo-proxy Length of output: 2172 🏁 Script executed: #!/bin/bash
# Check the cleanup implementation and how it uses TTL
rg -n "func.*cleanup|ttl" -A 8 --type go internal/provision/key_store.goRepository: EternisAI/silo-proxy Length of output: 917 🏁 Script executed: #!/bin/bash
# Look at the complete ProvisionConfig definition
cat -n cmd/silo-proxy-server/config.go | head -40Repository: EternisAI/silo-proxy Length of output: 1436 🏁 Script executed: #!/bin/bash
# Check if there's any config validation layer
rg -n "validate|Validate|Config\(\)" -B 2 -A 5 --type go cmd/silo-proxy-server/Repository: EternisAI/silo-proxy Length of output: 1366 🏁 Script executed: #!/bin/bash
# Check Go time.NewTicker behavior with zero or negative duration
# Search for any tests or documentation about this
rg -n "NewTicker.*0|NewTicker.*-" --type go | head -10Repository: EternisAI/silo-proxy Length of output: 46 🏁 Script executed: #!/bin/bash
# Check if there's a default configuration file or environment setup
find . -type f \( -name "*.yaml" -o -name "*.yml" -o -name "*.env*" -o -name "application*" \) 2>/dev/null | head -10Repository: EternisAI/silo-proxy Length of output: 236 🏁 Script executed: #!/bin/bash
# Check what values are used in tests or default configs
rg -n "key_ttl_hours|cleanup_interval_minutes" -A 1 -B 1 --type go --type yamlRepository: EternisAI/silo-proxy Length of output: 844 🏁 Script executed: #!/bin/bash
# Check Go's time.NewTicker behavior - let's see if there's any panic handling or test refs
rg -n "panic|NewTicker" -B 2 -A 2 --type go | grep -A 5 -B 5 "NewTicker"Repository: EternisAI/silo-proxy Length of output: 1375 🏁 Script executed: #!/bin/bash
# Get the full main.go context around the provisioning setup
sed -n '96,110p' cmd/silo-proxy-server/main.goRepository: EternisAI/silo-proxy Length of output: 638 Add validation for provisioning TTL and cleanup interval before initialization. The code directly passes Suggested fix if config.Provision.Enabled {
if certService == nil {
slog.Error("Provisioning requires TLS to be enabled")
os.Exit(1)
}
+ if config.Provision.KeyTTLHours <= 0 {
+ slog.Error("Provisioning requires key_ttl_hours to be > 0")
+ os.Exit(1)
+ }
+ if config.Provision.CleanupIntervalMinutes <= 0 {
+ slog.Error("Provisioning requires cleanup_interval_minutes to be > 0")
+ os.Exit(1)
+ }
ttl := time.Duration(config.Provision.KeyTTLHours) * time.Hour
keyStore = provision.NewKeyStore(ttl)
cleanupInterval := time.Duration(config.Provision.CleanupIntervalMinutes) * time.Minute
go keyStore.StartCleanup(context.Background(), cleanupInterval)
slog.Info("Provisioning enabled", "key_ttl_hours", config.Provision.KeyTTLHours)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| services := &internalhttp.Services{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GrpcServer: grpcSrv, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CertService: certService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AuthService: authService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UserService: userService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KeyStore: keyStore, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gin.SetMode(gin.ReleaseMode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate
AgentIDbefore using it in file paths.If the server returns an empty
AgentID, the code will create paths likecerts/agents//-cert.pemand a directory named with an empty string, which could cause unexpected behavior.🛡️ Proposed fix
var provResp dto.ProvisionResponse if err := json.Unmarshal(body, &provResp); err != nil { return fmt.Errorf("failed to parse response: %w", err) } + if provResp.AgentID == "" { + return fmt.Errorf("server returned empty agent ID") + }📝 Committable suggestion
🤖 Prompt for AI Agents