Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a3e31d5
Add cert service to create the CA cert
JckHoe Jan 27, 2026
3d05fe7
Add domain name config
JckHoe Jan 27, 2026
9da3412
Add function to GenerateAgentCert
JckHoe Jan 27, 2026
ace2945
Add the API handler for agent cert generate
JckHoe Jan 27, 2026
cf0a65f
Update main.go to init the cert service
JckHoe Jan 27, 2026
01086ee
Add provision agent cert script sample
JckHoe Jan 27, 2026
8f34201
change domain anmes and ip addresses config for tls provision to comm…
JckHoe Jan 27, 2026
87d3a75
Add CA key file config
JckHoe Jan 27, 2026
489d81d
bug fix on the CA key loading for the private key format
JckHoe Jan 27, 2026
2fdfa85
refactor the cert layer
JckHoe Jan 27, 2026
22c5f03
update GenerateServerCert signature
JckHoe Jan 27, 2026
06bfe85
update the Generate CA function
JckHoe Jan 27, 2026
94ad9f2
Clean up the cert KeyToPEM function to utils
JckHoe Jan 27, 2026
c7fe218
Add Clear all server certs API
JckHoe Jan 27, 2026
57338e7
Add simple curl
JckHoe Jan 27, 2026
e3fa4f2
Add simple curl
JckHoe Jan 27, 2026
6c1dfe9
simplify defer for zipwriter close
JckHoe Jan 27, 2026
7759bbb
bug fix on zip writer for agent generate
JckHoe Jan 27, 2026
0631367
Update domainnames config handling
JckHoe Jan 28, 2026
f08a6fd
Update API spec
JckHoe Jan 28, 2026
40487e6
Update the test scripts
JckHoe Jan 28, 2026
498f799
Update temp server cert clear
JckHoe Jan 28, 2026
6334b13
Update temp endpoint
JckHoe Jan 28, 2026
f9b4f1a
Fix findings
JckHoe Feb 4, 2026
a91121a
cleanup delete endpoint and add admin api key
JckHoe Feb 4, 2026
dc5b0ee
Update test scripts
JckHoe Feb 4, 2026
60a7854
Update error logging
JckHoe Feb 4, 2026
3c9eab9
use defer.Close()
lwlee2608 Feb 5, 2026
d6c3a0b
revert
lwlee2608 Feb 5, 2026
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
2 changes: 1 addition & 1 deletion cmd/silo-proxy-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func main() {
MaxAge: 12 * time.Hour,
}))
engine.Use(gin.Recovery())
internalhttp.SetupRoute(engine, services)
internalhttp.SetupRoute(engine, services, "")
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

Don’t hardcode an empty admin API key.
Passing "" means cert routes will always return 503 even if a key is configured. If the agent should support these endpoints, wire through the configured key; if not, consider explicitly disabling the routes.

✅ Suggested wiring
-internalhttp.SetupRoute(engine, services, "")
+internalhttp.SetupRoute(engine, services, config.Http.AdminAPIKey)
📝 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
internalhttp.SetupRoute(engine, services, "")
internalhttp.SetupRoute(engine, services, config.Http.AdminAPIKey)
🤖 Prompt for AI Agents
In `@cmd/silo-proxy-agent/main.go` at line 54, The call
internalhttp.SetupRoute(engine, services, "") is hardcoding an empty admin API
key which forces cert routes to 503; replace the literal "" with the actual
configured admin API key (e.g. pull it from the agent configuration or services'
config like services.Config.AdminAPIKey or a configuredAdminAPIKey variable) so
the routes are enabled when a key is present, or explicitly disable the routes
by passing a clear disable flag/option instead of an empty string if you intend
to keep them off.


server := &http.Server{
Addr: fmt.Sprintf(":%d", config.Http.Port),
Expand Down
5 changes: 5 additions & 0 deletions cmd/silo-proxy-server/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ log:
level: debug
http:
port: 8080
admin_api_key: "" # Required for certificate management endpoints
agent_port_range:
start: 8100
end: 8100
Expand All @@ -12,4 +13,8 @@ grpc:
cert_file: ./certs/server/server-cert.pem
key_file: ./certs/server/server-key.pem
ca_file: ./certs/ca/ca-cert.pem
ca_key_file: ./certs/ca/ca-key.pem
client_auth: require
domain_names: "localhost"
ip_addresses: "127.0.0.1"
agent_cert_dir: ./certs/agents
14 changes: 9 additions & 5 deletions cmd/silo-proxy-server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ type GrpcConfig struct {
}

type TLSConfig struct {
Enabled bool `mapstructure:"enabled"`
CertFile string `mapstructure:"cert_file"`
KeyFile string `mapstructure:"key_file"`
CAFile string `mapstructure:"ca_file"`
ClientAuth string `mapstructure:"client_auth"`
Enabled bool `mapstructure:"enabled"`
CertFile string `mapstructure:"cert_file"`
KeyFile string `mapstructure:"key_file"`
CAFile string `mapstructure:"ca_file"`
CAKeyFile string `mapstructure:"ca_key_file"`
ClientAuth string `mapstructure:"client_auth"`
DomainNames string `mapstructure:"domain_names"`
IPAddresses string `mapstructure:"ip_addresses"`
AgentCertDir string `mapstructure:"agent_cert_dir"`
}

var config Config
Expand Down
27 changes: 24 additions & 3 deletions cmd/silo-proxy-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

internalhttp "github.com/EternisAI/silo-proxy/internal/api/http"
"github.com/EternisAI/silo-proxy/internal/cert"
grpcserver "github.com/EternisAI/silo-proxy/internal/grpc/server"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
Expand All @@ -32,6 +33,25 @@ func main() {
ClientAuth: config.Grpc.TLS.ClientAuth,
}

var certService *cert.Service
if config.Grpc.TLS.Enabled {

var err error
certService, err = cert.New(
config.Grpc.TLS.CAFile,
config.Grpc.TLS.CAKeyFile,
config.Grpc.TLS.CertFile,
config.Grpc.TLS.KeyFile,
config.Grpc.TLS.AgentCertDir,
config.Grpc.TLS.DomainNames,
config.Grpc.TLS.IPAddresses,
)
if err != nil {
slog.Error("Failed to initialize certificates", "error", err)
os.Exit(1)
}
}

grpcSrv := grpcserver.NewServer(config.Grpc.Port, tlsConfig)

portManager, err := internalhttp.NewPortManager(
Expand All @@ -52,21 +72,22 @@ func main() {
"pool_size", config.Http.AgentPortRange.End-config.Http.AgentPortRange.Start+1)

services := &internalhttp.Services{
GrpcServer: grpcSrv,
GrpcServer: grpcSrv,
CertService: certService,
}

gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization", "X-API-Key"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
engine.Use(gin.Recovery())
internalhttp.SetupRoute(engine, services)
internalhttp.SetupRoute(engine, services, config.Http.AdminAPIKey)

httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", config.Http.Port),
Expand Down
5 changes: 5 additions & 0 deletions docs/potential_issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Potential Issues

1. Missing server certificate regeneration when domain/IP config changes
2. No rate limiting on certificate generation endpoints
3. Agent cert directory cleanup - DeleteAgentCert removes entire directory, which could be dangerous
234 changes: 234 additions & 0 deletions internal/api/http/handler/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package handler

import (
"archive/zip"
"bytes"
"crypto/rsa"
"crypto/x509"
"fmt"
"log/slog"
"net/http"

"github.com/EternisAI/silo-proxy/internal/cert"
"github.com/gin-gonic/gin"
)

func (h *CertHandler) validateAgentID(ctx *gin.Context) (string, bool) {
agentID := ctx.Param("id")
if err := cert.ValidateAgentID(agentID); err != nil {
slog.Warn("Invalid agent ID", "agent_id", agentID, "error", err)
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return "", false
}
return agentID, true
}

type CertHandler struct {
certService *cert.Service
}

func NewCertHandler(certService *cert.Service) *CertHandler {
return &CertHandler{
certService: certService,
}
}

func (h *CertHandler) CreateAgentCertificate(ctx *gin.Context) {
if h.certService == nil {
slog.Warn("Agent cert creation requested but TLS is disabled")
ctx.JSON(http.StatusBadRequest, gin.H{
"error": "TLS is not enabled on this server",
})
return
}

agentID, ok := h.validateAgentID(ctx)
if !ok {
return
}

if h.certService.AgentCertExists(agentID) {
slog.Warn("Certificate already exists", "agent_id", agentID)
ctx.JSON(http.StatusConflict, gin.H{
"error": "Certificate already exists for this agent",
})
return
}

slog.Info("Creating agent certificate", "agent_id", agentID)

agentCert, agentKey, err := h.certService.GenerateAgentCert(agentID)
if err != nil {
slog.Error("Failed to generate agent certificate", "error", err, "agent_id", agentID)
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to generate agent certificate",
})
return
}

caCertBytes, err := h.certService.GetCACert()
if err != nil {
slog.Error("Failed to read CA certificate", "error", err, "agent_id", agentID)
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to read CA certificate",
})
return
}

zipBuffer, err := h.createCertZip(agentID, agentCert, agentKey, caCertBytes)
if err != nil {
slog.Error("Failed to create zip file", "error", err, "agent_id", agentID)
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create zip file",
})
return
}

ctx.Header("Content-Type", "application/zip")
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-certs.zip\"", agentID))
ctx.Data(http.StatusCreated, "application/zip", zipBuffer.Bytes())

slog.Info("Agent certificate created successfully", "agent_id", agentID, "zip_size", zipBuffer.Len())
}

func (h *CertHandler) GetAgentCertificate(ctx *gin.Context) {
if h.certService == nil {
slog.Warn("Agent cert retrieval requested but TLS is disabled")
ctx.JSON(http.StatusBadRequest, gin.H{
"error": "TLS is not enabled on this server",
})
return
}

agentID, ok := h.validateAgentID(ctx)
if !ok {
return
}

if !h.certService.AgentCertExists(agentID) {
slog.Warn("Certificate not found", "agent_id", agentID)
ctx.JSON(http.StatusNotFound, gin.H{
"error": "Certificate not found for this agent",
})
return
}

slog.Info("Retrieving agent certificate", "agent_id", agentID)

agentCertBytes, agentKeyBytes, err := h.certService.GetAgentCert(agentID)
if err != nil {
slog.Error("Failed to read agent certificate", "error", err, "agent_id", agentID)
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to read agent certificate",
})
return
}

caCertBytes, err := h.certService.GetCACert()
if err != nil {
slog.Error("Failed to read CA certificate", "error", err, "agent_id", agentID)
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to read CA certificate",
})
return
}

zipBuffer, err := h.createCertZipFromBytes(agentID, agentCertBytes, agentKeyBytes, caCertBytes)
if err != nil {
slog.Error("Failed to create zip file", "error", err, "agent_id", agentID)
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create zip file",
})
return
}

ctx.Header("Content-Type", "application/zip")
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-certs.zip\"", agentID))
ctx.Data(http.StatusOK, "application/zip", zipBuffer.Bytes())

slog.Info("Agent certificate retrieved successfully", "agent_id", agentID, "zip_size", zipBuffer.Len())
}

func (h *CertHandler) DeleteAgentCertificate(ctx *gin.Context) {
if h.certService == nil {
slog.Warn("Agent cert deletion requested but TLS is disabled")
ctx.JSON(http.StatusBadRequest, gin.H{
"error": "TLS is not enabled on this server",
})
return
}

agentID, ok := h.validateAgentID(ctx)
if !ok {
return
}

if !h.certService.AgentCertExists(agentID) {
slog.Warn("Certificate not found for deletion", "agent_id", agentID)
ctx.JSON(http.StatusNotFound, gin.H{
"error": "Certificate not found for this agent",
})
return
}

slog.Info("Deleting agent certificate", "agent_id", agentID)

certDir := h.certService.GetAgentCertDir(agentID)
if err := h.certService.DeleteAgentCert(agentID); err != nil {
slog.Error("Failed to delete agent certificate", "error", err, "agent_id", agentID)
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to delete agent certificate",
})
return
}

ctx.JSON(http.StatusOK, gin.H{
"message": "Successfully deleted agent certificate",
"deleted_paths": []string{certDir},
})
slog.Info("Agent certificate deleted successfully", "agent_id", agentID)
}

func (h *CertHandler) createCertZip(agentID string, agentCert *x509.Certificate, agentKey *rsa.PrivateKey, caCertBytes []byte) (*bytes.Buffer, error) {
agentCertPEM, err := cert.CertToPEM(agentCert)
if err != nil {
return nil, fmt.Errorf("failed to encode agent certificate: %w", err)
}

agentKeyPEM, err := cert.KeyToPEM(agentKey)
if err != nil {
return nil, fmt.Errorf("failed to encode agent key: %w", err)
}

return h.createCertZipFromBytes(agentID, agentCertPEM, agentKeyPEM, caCertBytes)
}

func (h *CertHandler) createCertZipFromBytes(agentID string, certPEM, keyPEM, caCertBytes []byte) (*bytes.Buffer, error) {
zipBuffer := new(bytes.Buffer)
zipWriter := zip.NewWriter(zipBuffer)
defer zipWriter.Close()

files := map[string][]byte{
fmt.Sprintf("%s-cert.pem", agentID): certPEM,
fmt.Sprintf("%s-key.pem", agentID): keyPEM,
"ca-cert.pem": caCertBytes,
}

for filename, content := range files {
f, err := zipWriter.Create(filename)
if err != nil {
return nil, fmt.Errorf("failed to create zip entry for %s: %w", filename, err)
}
if _, err := f.Write(content); err != nil {
return nil, fmt.Errorf("failed to write zip entry for %s: %w", filename, err)
}
}

if err := zipWriter.Close(); err != nil {
return nil, fmt.Errorf("failed to finalize zip archive: %w", err)
}

return zipBuffer, nil
}
1 change: 1 addition & 0 deletions internal/api/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package http
type Config struct {
Port uint `mapstructure:"port"`
AgentPortRange PortRange `mapstructure:"agent_port_range"`
AdminAPIKey string `mapstructure:"admin_api_key"`
}

type PortRange struct {
Expand Down
Loading