Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/silo-proxy-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import (
var AppVersion string

func main() {
if len(os.Args) > 1 && os.Args[1] == "provision" {
if err := runProvision(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
return
}

InitConfig()

slog.Info("Silo Proxy Agent", "version", AppVersion)
Expand Down
113 changes: 113 additions & 0 deletions cmd/silo-proxy-agent/provision.go
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)
}
Comment on lines +67 to +70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate AgentID before using it in file paths.

If the server returns an empty AgentID, the code will create paths like certs/agents//-cert.pem and 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var provResp dto.ProvisionResponse
if err := json.Unmarshal(body, &provResp); err != nil {
return fmt.Errorf("failed to parse response: %w", err)
}
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")
}
🤖 Prompt for AI Agents
In `@cmd/silo-proxy-agent/provision.go` around lines 67 - 70, The code unmarshals
into provResp (dto.ProvisionResponse) but does not validate provResp.AgentID
before using it to build file paths; add a check immediately after
json.Unmarshal to trim whitespace and ensure provResp.AgentID is non-empty and
contains no path separators (e.g., reject values with "/" or "\"), returning a
descriptive error if invalid, so subsequent code that constructs
certs/agents/<AgentID>-cert.pem or creates directories will not operate on an
empty or malicious agent id.


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
}
4 changes: 4 additions & 0 deletions cmd/silo-proxy-server/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ grpc:
domain_names: "localhost"
ip_addresses: "127.0.0.1"
agent_cert_dir: ./certs/agents
provision:
enabled: false
key_ttl_hours: 24
cleanup_interval_minutes: 60
17 changes: 12 additions & 5 deletions cmd/silo-proxy-server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ import (
)

type Config struct {
Log LogConfig
Http http.Config
Grpc GrpcConfig
DB db.Config `mapstructure:"db"`
JWT auth.Config `mapstructure:"jwt"`
Log LogConfig
Http http.Config
Grpc GrpcConfig
DB db.Config `mapstructure:"db"`
JWT auth.Config `mapstructure:"jwt"`
Provision ProvisionConfig `mapstructure:"provision"`
}

type ProvisionConfig struct {
Enabled bool `mapstructure:"enabled"`
KeyTTLHours int `mapstructure:"key_ttl_hours"`
CleanupIntervalMinutes int `mapstructure:"cleanup_interval_minutes"`
}

type GrpcConfig struct {
Expand Down
15 changes: 15 additions & 0 deletions cmd/silo-proxy-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -5

Repository: 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 go

Repository: 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 -20

Repository: 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 go

Repository: 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 -20

Repository: 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 go

Repository: 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 go

Repository: 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.go

Repository: 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 -40

Repository: 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 -10

Repository: 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 -10

Repository: 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 yaml

Repository: 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.go

Repository: EternisAI/silo-proxy

Length of output: 638


Add validation for provisioning TTL and cleanup interval before initialization.

The code directly passes config.Provision.KeyTTLHours and config.Provision.CleanupIntervalMinutes to duration calculations without validation. If either is zero or negative, time.NewTicker will panic, and keys will be created with immediate expiry. Add explicit validation to fail fast when provisioning is enabled.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
}
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)
}
🤖 Prompt for AI Agents
In `@cmd/silo-proxy-server/main.go` around lines 96 - 107, When provisioning is
enabled, validate config.Provision.KeyTTLHours and
config.Provision.CleanupIntervalMinutes before creating provision.NewKeyStore
and starting keyStore.StartCleanup: ensure both values are > 0 (reject
zero/negative), log a clear error via slog.Error and os.Exit(1) if invalid, and
only then compute ttl := time.Duration(config.Provision.KeyTTLHours)*time.Hour,
cleanupInterval :=
time.Duration(config.Provision.CleanupIntervalMinutes)*time.Minute and proceed
to call provision.NewKeyStore and go keyStore.StartCleanup(context.Background(),
cleanupInterval); keep existing certService nil check intact.


services := &internalhttp.Services{
GrpcServer: grpcSrv,
CertService: certService,
AuthService: authService,
UserService: userService,
KeyStore: keyStore,
}

gin.SetMode(gin.ReleaseMode)
Expand Down
Loading