From a3e31d5c62306af6ac06a19ad09bb09043d01589 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Tue, 27 Jan 2026 23:01:05 +0800 Subject: [PATCH 01/29] Add cert service to create the CA cert --- internal/cert/service.go | 283 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 internal/cert/service.go diff --git a/internal/cert/service.go b/internal/cert/service.go new file mode 100644 index 0000000..5310a29 --- /dev/null +++ b/internal/cert/service.go @@ -0,0 +1,283 @@ +package cert + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log/slog" + "math/big" + "net" + "os" + "path/filepath" + "time" +) + +type Service struct { + CaCertPath string + CaKeyPath string + ServerCertPath string + ServerKeyPath string +} + +func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath string) (*Service, error) { + s := &Service{ + CaCertPath: caCertPath, + CaKeyPath: caKeyPath, + ServerCertPath: serverCertPath, + ServerKeyPath: serverKeyPath, + } + + if err := s.ensureCertificates(); err != nil { + return nil, fmt.Errorf("failed to ensure certificates: %w", err) + } + + return s, nil +} + +func (s *Service) ensureCertificates() error { + caCertExists := fileExists(s.CaCertPath) + caKeyExists := fileExists(s.CaKeyPath) + + var caCert *x509.Certificate + var caKey *rsa.PrivateKey + + if !caCertExists || !caKeyExists { + slog.Info("CA certificate not found, generating new CA", "cert_path", s.CaCertPath) + + var err error + caCert, caKey, err = generateCA() + if err != nil { + slog.Error("Failed to generate CA certificate", "error", err) + return fmt.Errorf("failed to generate CA certificate: %w", err) + } + + if err := s.ensureDirectory(s.CaCertPath); err != nil { + return err + } + + if err := writeCertToFile(caCert, s.CaCertPath); err != nil { + slog.Error("Failed to write CA certificate", "error", err, "path", s.CaCertPath) + return fmt.Errorf("failed to write CA certificate: %w", err) + } + + if err := s.ensureDirectory(s.CaKeyPath); err != nil { + return err + } + + if err := writeKeyToFile(caKey, s.CaKeyPath); err != nil { + slog.Error("Failed to write CA key", "error", err, "path", s.CaKeyPath) + return fmt.Errorf("failed to write CA key: %w", err) + } + + slog.Info("Generated CA certificate", "cert_path", s.CaCertPath, "key_path", s.CaKeyPath) + } else { + slog.Debug("Using existing CA certificate", "cert_path", s.CaCertPath) + + var err error + caCert, caKey, err = loadCA(s.CaCertPath, s.CaKeyPath) + if err != nil { + slog.Error("Failed to load existing CA certificate", "error", err) + return fmt.Errorf("failed to load existing CA certificate: %w", err) + } + } + + serverCertExists := fileExists(s.ServerCertPath) + serverKeyExists := fileExists(s.ServerKeyPath) + + if !serverCertExists || !serverKeyExists { + slog.Info("Server certificate not found, generating new server certificate", "cert_path", s.ServerCertPath) + + serverCert, serverKey, err := generateServerCert(caCert, caKey) + if err != nil { + slog.Error("Failed to generate server certificate", "error", err) + return fmt.Errorf("failed to generate server certificate: %w", err) + } + + if err := s.ensureDirectory(s.ServerCertPath); err != nil { + return err + } + + if err := writeCertToFile(serverCert, s.ServerCertPath); err != nil { + slog.Error("Failed to write server certificate", "error", err, "path", s.ServerCertPath) + return fmt.Errorf("failed to write server certificate: %w", err) + } + + if err := s.ensureDirectory(s.ServerKeyPath); err != nil { + return err + } + + if err := writeKeyToFile(serverKey, s.ServerKeyPath); err != nil { + slog.Error("Failed to write server key", "error", err, "path", s.ServerKeyPath) + return fmt.Errorf("failed to write server key: %w", err) + } + + slog.Info("Generated server certificate", "cert_path", s.ServerCertPath, "key_path", s.ServerKeyPath) + } else { + slog.Debug("Using existing server certificate", "cert_path", s.ServerCertPath) + } + + return nil +} + +func (s *Service) ensureDirectory(filePath string) error { + dir := filepath.Dir(filePath) + if err := os.MkdirAll(dir, 0755); err != nil { + slog.Error("Failed to create directory", "error", err, "path", dir) + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + return nil +} + +func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) { + caKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate CA key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) + } + + caTemplate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Silo Proxy CA"}, + CommonName: "Silo Proxy Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + + caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create CA certificate: %w", err) + } + + caCert, err := x509.ParseCertificate(caCertBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err) + } + + return caCert, caKey, nil +} + +func generateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) { + serverKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate server key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) + } + + serverTemplate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Silo Proxy"}, + CommonName: "localhost", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + } + + serverCertBytes, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create server certificate: %w", err) + } + + serverCert, err := x509.ParseCertificate(serverCertBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse server certificate: %w", err) + } + + return serverCert, serverKey, nil +} + +func loadCA(certPath, keyPath string) (*x509.Certificate, *rsa.PrivateKey, error) { + certBytes, err := os.ReadFile(certPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to read CA certificate: %w", err) + } + + certBlock, _ := pem.Decode(certBytes) + if certBlock == nil { + return nil, nil, fmt.Errorf("failed to decode CA certificate PEM") + } + + caCert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err) + } + + keyBytes, err := os.ReadFile(keyPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to read CA key: %w", err) + } + + keyBlock, _ := pem.Decode(keyBytes) + if keyBlock == nil { + return nil, nil, fmt.Errorf("failed to decode CA key PEM") + } + + caKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse CA key: %w", err) + } + + return caCert, caKey, nil +} + +func writeCertToFile(cert *x509.Certificate, path string) error { + certFile, err := os.Create(path) + if err != nil { + return fmt.Errorf("failed to create certificate file: %w", err) + } + defer certFile.Close() + + if err := pem.Encode(certFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }); err != nil { + return fmt.Errorf("failed to encode certificate: %w", err) + } + + return nil +} + +func writeKeyToFile(key *rsa.PrivateKey, path string) error { + keyFile, err := os.Create(path) + if err != nil { + return fmt.Errorf("failed to create key file: %w", err) + } + defer keyFile.Close() + + if err := pem.Encode(keyFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }); err != nil { + return fmt.Errorf("failed to encode key: %w", err) + } + + return nil +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} From 3d05fe7c04c6cb2721e6eb9c1bcc39ae15fe47d2 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Tue, 27 Jan 2026 23:08:47 +0800 Subject: [PATCH 02/29] Add domain name config --- cmd/silo-proxy-server/application.yml | 4 +++ cmd/silo-proxy-server/config.go | 12 ++++---- cmd/silo-proxy-server/main.go | 32 ++++++++++++++++++++ internal/cert/service.go | 42 ++++++++++++++++++++++----- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/cmd/silo-proxy-server/application.yml b/cmd/silo-proxy-server/application.yml index 5cd6e7f..21deaee 100644 --- a/cmd/silo-proxy-server/application.yml +++ b/cmd/silo-proxy-server/application.yml @@ -13,3 +13,7 @@ grpc: key_file: ./certs/server/server-key.pem ca_file: ./certs/ca/ca-cert.pem client_auth: require + domain_names: + - localhost + ip_addresses: + - 127.0.0.1 diff --git a/cmd/silo-proxy-server/config.go b/cmd/silo-proxy-server/config.go index 611a06e..9b810b7 100644 --- a/cmd/silo-proxy-server/config.go +++ b/cmd/silo-proxy-server/config.go @@ -22,11 +22,13 @@ 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"` + ClientAuth string `mapstructure:"client_auth"` + DomainNames []string `mapstructure:"domain_names"` + IPAddresses []string `mapstructure:"ip_addresses"` } var config Config diff --git a/cmd/silo-proxy-server/main.go b/cmd/silo-proxy-server/main.go index 7c75dd0..3af5f48 100644 --- a/cmd/silo-proxy-server/main.go +++ b/cmd/silo-proxy-server/main.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "net" "net/http" "os" "os/signal" @@ -12,6 +13,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" @@ -32,6 +34,36 @@ func main() { ClientAuth: config.Grpc.TLS.ClientAuth, } + if config.Grpc.TLS.Enabled { + certOpts := &cert.Options{} + + if len(config.Grpc.TLS.DomainNames) > 0 { + certOpts.DomainNames = config.Grpc.TLS.DomainNames + } + + if len(config.Grpc.TLS.IPAddresses) > 0 { + for _, ipStr := range config.Grpc.TLS.IPAddresses { + if ip := net.ParseIP(ipStr); ip != nil { + certOpts.IPAddresses = append(certOpts.IPAddresses, ip) + } else { + slog.Warn("Invalid IP address in configuration, skipping", "ip", ipStr) + } + } + } + + _, err := cert.New( + config.Grpc.TLS.CAFile, + "./certs/ca/ca-key.pem", + config.Grpc.TLS.CertFile, + config.Grpc.TLS.KeyFile, + certOpts, + ) + 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( diff --git a/internal/cert/service.go b/internal/cert/service.go index 5310a29..673d2eb 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -20,9 +20,16 @@ type Service struct { CaKeyPath string ServerCertPath string ServerKeyPath string + DomainNames []string + IPAddresses []net.IP } -func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath string) (*Service, error) { +type Options struct { + DomainNames []string + IPAddresses []net.IP +} + +func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath string, opts *Options) (*Service, error) { s := &Service{ CaCertPath: caCertPath, CaKeyPath: caKeyPath, @@ -30,6 +37,19 @@ func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath string) (*Service, ServerKeyPath: serverKeyPath, } + if opts != nil { + s.DomainNames = opts.DomainNames + s.IPAddresses = opts.IPAddresses + } + + if len(s.DomainNames) == 0 { + s.DomainNames = []string{"localhost"} + } + + if len(s.IPAddresses) == 0 { + s.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} + } + if err := s.ensureCertificates(); err != nil { return nil, fmt.Errorf("failed to ensure certificates: %w", err) } @@ -88,9 +108,12 @@ func (s *Service) ensureCertificates() error { serverKeyExists := fileExists(s.ServerKeyPath) if !serverCertExists || !serverKeyExists { - slog.Info("Server certificate not found, generating new server certificate", "cert_path", s.ServerCertPath) + slog.Info("Server certificate not found, generating new server certificate", + "cert_path", s.ServerCertPath, + "domains", s.DomainNames, + "ips", s.IPAddresses) - serverCert, serverKey, err := generateServerCert(caCert, caKey) + serverCert, serverKey, err := generateServerCert(caCert, caKey, s.DomainNames, s.IPAddresses) if err != nil { slog.Error("Failed to generate server certificate", "error", err) return fmt.Errorf("failed to generate server certificate: %w", err) @@ -170,7 +193,7 @@ func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) { return caCert, caKey, nil } -func generateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) { +func generateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey, domainNames []string, ipAddresses []net.IP) (*x509.Certificate, *rsa.PrivateKey, error) { serverKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, nil, fmt.Errorf("failed to generate server key: %w", err) @@ -181,19 +204,24 @@ func generateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509. return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) } + commonName := "localhost" + if len(domainNames) > 0 { + commonName = domainNames[0] + } + serverTemplate := &x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{"Silo Proxy"}, - CommonName: "localhost", + CommonName: commonName, }, NotBefore: time.Now(), NotAfter: time.Now().Add(365 * 24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, - DNSNames: []string{"localhost"}, - IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + DNSNames: domainNames, + IPAddresses: ipAddresses, } serverCertBytes, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) From 9da341205a2bd2cd3cdb58afd68b371661cdf783 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:09:00 +0800 Subject: [PATCH 03/29] Add function to GenerateAgentCert --- internal/cert/service.go | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/internal/cert/service.go b/internal/cert/service.go index 673d2eb..77d77cd 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -309,3 +309,57 @@ func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } + +func (s *Service) GenerateAgentCert(agentID string) (*x509.Certificate, *rsa.PrivateKey, error) { + slog.Info("Generating agent certificate", "agent_id", agentID) + + caCert, caKey, err := loadCA(s.CaCertPath, s.CaKeyPath) + if err != nil { + slog.Error("Failed to load CA for agent cert generation", "error", err, "agent_id", agentID) + return nil, nil, fmt.Errorf("failed to load CA: %w", err) + } + + agentKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate agent key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) + } + + agentTemplate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Silo Proxy"}, + CommonName: agentID, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + agentCertBytes, err := x509.CreateCertificate(rand.Reader, agentTemplate, caCert, &agentKey.PublicKey, caKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create agent certificate: %w", err) + } + + agentCert, err := x509.ParseCertificate(agentCertBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse agent certificate: %w", err) + } + + slog.Info("Generated agent certificate", "agent_id", agentID) + return agentCert, agentKey, nil +} + +func (s *Service) GetCACert() ([]byte, error) { + certBytes, err := os.ReadFile(s.CaCertPath) + if err != nil { + return nil, fmt.Errorf("failed to read CA certificate: %w", err) + } + return certBytes, nil +} From ace29455b308300b4ee2862a815782ac4a56cc76 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:09:17 +0800 Subject: [PATCH 04/29] Add the API handler for agent cert generate --- internal/api/http/dto/cert.go | 5 + internal/api/http/handler/cert.go | 152 ++++++++++++++++++++++++++++++ internal/api/http/router.go | 7 +- 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 internal/api/http/dto/cert.go create mode 100644 internal/api/http/handler/cert.go diff --git a/internal/api/http/dto/cert.go b/internal/api/http/dto/cert.go new file mode 100644 index 0000000..3f93954 --- /dev/null +++ b/internal/api/http/dto/cert.go @@ -0,0 +1,5 @@ +package dto + +type ProvisionAgentRequest struct { + AgentID string `json:"agent_id" binding:"required"` +} diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go new file mode 100644 index 0000000..4e29296 --- /dev/null +++ b/internal/api/http/handler/cert.go @@ -0,0 +1,152 @@ +package handler + +import ( + "archive/zip" + "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "log/slog" + "net/http" + + "github.com/EternisAI/silo-proxy/internal/api/http/dto" + "github.com/EternisAI/silo-proxy/internal/cert" + "github.com/gin-gonic/gin" +) + +type CertHandler struct { + certService *cert.Service +} + +func NewCertHandler(certService *cert.Service) *CertHandler { + return &CertHandler{ + certService: certService, + } +} + +func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { + if h.certService == nil { + slog.Warn("Agent cert provisioning requested but TLS is disabled") + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "TLS is not enabled on this server", + }) + return + } + + var req dto.ProvisionAgentRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid request: %v", err), + }) + return + } + + if req.AgentID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "agent_id is required", + }) + return + } + + slog.Info("Provisioning agent certificates", "agent_id", req.AgentID) + + agentCert, agentKey, err := h.certService.GenerateAgentCert(req.AgentID) + if err != nil { + slog.Error("Failed to generate agent certificate", "error", err, "agent_id", req.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", req.AgentID) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to read CA certificate", + }) + return + } + + zipBuffer := new(bytes.Buffer) + zipWriter := zip.NewWriter(zipBuffer) + + agentCertPEM, err := certToPEM(agentCert) + if err != nil { + slog.Error("Failed to encode agent certificate", "error", err, "agent_id", req.AgentID) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to encode agent certificate", + }) + return + } + + agentKeyPEM, err := keyToPEM(agentKey) + if err != nil { + slog.Error("Failed to encode agent key", "error", err, "agent_id", req.AgentID) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to encode agent key", + }) + return + } + + files := map[string][]byte{ + fmt.Sprintf("%s-cert.pem", req.AgentID): agentCertPEM, + fmt.Sprintf("%s-key.pem", req.AgentID): agentKeyPEM, + "ca-cert.pem": caCertBytes, + } + + for filename, content := range files { + f, err := zipWriter.Create(filename) + if err != nil { + slog.Error("Failed to create zip file entry", "error", err, "filename", filename) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create zip file", + }) + return + } + if _, err := f.Write(content); err != nil { + slog.Error("Failed to write to zip file", "error", err, "filename", filename) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to write zip file", + }) + return + } + } + + if err := zipWriter.Close(); err != nil { + slog.Error("Failed to close zip writer", "error", err) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to finalize zip file", + }) + return + } + + ctx.Header("Content-Type", "application/zip") + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-certs.zip\"", req.AgentID)) + ctx.Data(http.StatusOK, "application/zip", zipBuffer.Bytes()) + + slog.Info("Agent certificates provisioned successfully", "agent_id", req.AgentID, "zip_size", zipBuffer.Len()) +} + +func certToPEM(cert *x509.Certificate) ([]byte, error) { + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func keyToPEM(key *rsa.PrivateKey) ([]byte, error) { + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/internal/api/http/router.go b/internal/api/http/router.go index 4940f19..fa3c291 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -3,12 +3,14 @@ package http import ( "github.com/EternisAI/silo-proxy/internal/api/http/handler" "github.com/EternisAI/silo-proxy/internal/api/http/middleware" + "github.com/EternisAI/silo-proxy/internal/cert" grpcserver "github.com/EternisAI/silo-proxy/internal/grpc/server" "github.com/gin-gonic/gin" ) type Services struct { - GrpcServer *grpcserver.Server + GrpcServer *grpcserver.Server + CertService *cert.Service } func SetupRoute(engine *gin.Engine, srvs *Services) { @@ -21,4 +23,7 @@ func SetupRoute(engine *gin.Engine, srvs *Services) { adminHandler := handler.NewAdminHandler(srvs.GrpcServer) engine.GET("/agents", adminHandler.ListAgents) } + + certHandler := handler.NewCertHandler(srvs.CertService) + engine.POST("/cert/agent", certHandler.ProvisionAgent) } From cf0a65f05c7f32090351b1007237d46cf8c4e104 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:09:26 +0800 Subject: [PATCH 05/29] Update main.go to init the cert service --- cmd/silo-proxy-server/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/silo-proxy-server/main.go b/cmd/silo-proxy-server/main.go index 3af5f48..c7053c3 100644 --- a/cmd/silo-proxy-server/main.go +++ b/cmd/silo-proxy-server/main.go @@ -34,6 +34,7 @@ func main() { ClientAuth: config.Grpc.TLS.ClientAuth, } + var certService *cert.Service if config.Grpc.TLS.Enabled { certOpts := &cert.Options{} @@ -51,7 +52,8 @@ func main() { } } - _, err := cert.New( + var err error + certService, err = cert.New( config.Grpc.TLS.CAFile, "./certs/ca/ca-key.pem", config.Grpc.TLS.CertFile, @@ -84,7 +86,8 @@ 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) From 01086ee455b6bd29462395a694d4389e5ae06af4 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:09:36 +0800 Subject: [PATCH 06/29] Add provision agent cert script sample --- misc/provision-agent-cert.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 misc/provision-agent-cert.sh diff --git a/misc/provision-agent-cert.sh b/misc/provision-agent-cert.sh new file mode 100755 index 0000000..00e1a44 --- /dev/null +++ b/misc/provision-agent-cert.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X POST http://localhost:8080/cert/agent -H "Content-Type: application/json" -d '{"agent_id": "agent-1"}' --output agent-1-certs.zip From 8f34201681bcfb2ac41a9839f02ef177a97c1e21 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:21:50 +0800 Subject: [PATCH 07/29] change domain anmes and ip addresses config for tls provision to comma separated --- cmd/silo-proxy-server/application.yml | 6 ++---- cmd/silo-proxy-server/config.go | 28 ++++++++++++++++++++------- cmd/silo-proxy-server/main.go | 10 ++++++---- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/cmd/silo-proxy-server/application.yml b/cmd/silo-proxy-server/application.yml index 21deaee..4a8856b 100644 --- a/cmd/silo-proxy-server/application.yml +++ b/cmd/silo-proxy-server/application.yml @@ -13,7 +13,5 @@ grpc: key_file: ./certs/server/server-key.pem ca_file: ./certs/ca/ca-cert.pem client_auth: require - domain_names: - - localhost - ip_addresses: - - 127.0.0.1 + domain_names: "localhost" + ip_addresses: "127.0.0.1" diff --git a/cmd/silo-proxy-server/config.go b/cmd/silo-proxy-server/config.go index 9b810b7..527a494 100644 --- a/cmd/silo-proxy-server/config.go +++ b/cmd/silo-proxy-server/config.go @@ -22,17 +22,31 @@ 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"` - DomainNames []string `mapstructure:"domain_names"` - IPAddresses []string `mapstructure:"ip_addresses"` + Enabled bool `mapstructure:"enabled"` + CertFile string `mapstructure:"cert_file"` + KeyFile string `mapstructure:"key_file"` + CAFile string `mapstructure:"ca_file"` + ClientAuth string `mapstructure:"client_auth"` + DomainNames string `mapstructure:"domain_names"` + IPAddresses string `mapstructure:"ip_addresses"` } var config Config +func ParseCommaSeparated(input string) []string { + if input == "" { + return nil + } + parts := strings.Split(input, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + func InitConfig() { var err error diff --git a/cmd/silo-proxy-server/main.go b/cmd/silo-proxy-server/main.go index c7053c3..ed46fa6 100644 --- a/cmd/silo-proxy-server/main.go +++ b/cmd/silo-proxy-server/main.go @@ -38,12 +38,14 @@ func main() { if config.Grpc.TLS.Enabled { certOpts := &cert.Options{} - if len(config.Grpc.TLS.DomainNames) > 0 { - certOpts.DomainNames = config.Grpc.TLS.DomainNames + domainNames := ParseCommaSeparated(config.Grpc.TLS.DomainNames) + if len(domainNames) > 0 { + certOpts.DomainNames = domainNames } - if len(config.Grpc.TLS.IPAddresses) > 0 { - for _, ipStr := range config.Grpc.TLS.IPAddresses { + ipAddresses := ParseCommaSeparated(config.Grpc.TLS.IPAddresses) + if len(ipAddresses) > 0 { + for _, ipStr := range ipAddresses { if ip := net.ParseIP(ipStr); ip != nil { certOpts.IPAddresses = append(certOpts.IPAddresses, ip) } else { From 87d3a75cd179127a79a640ab90df641de7f05c4f Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:24:57 +0800 Subject: [PATCH 08/29] Add CA key file config --- cmd/silo-proxy-server/application.yml | 1 + cmd/silo-proxy-server/config.go | 1 + cmd/silo-proxy-server/main.go | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/silo-proxy-server/application.yml b/cmd/silo-proxy-server/application.yml index 4a8856b..d737f9c 100644 --- a/cmd/silo-proxy-server/application.yml +++ b/cmd/silo-proxy-server/application.yml @@ -12,6 +12,7 @@ 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" diff --git a/cmd/silo-proxy-server/config.go b/cmd/silo-proxy-server/config.go index 527a494..4bfb911 100644 --- a/cmd/silo-proxy-server/config.go +++ b/cmd/silo-proxy-server/config.go @@ -26,6 +26,7 @@ type TLSConfig struct { 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"` diff --git a/cmd/silo-proxy-server/main.go b/cmd/silo-proxy-server/main.go index ed46fa6..65b35a1 100644 --- a/cmd/silo-proxy-server/main.go +++ b/cmd/silo-proxy-server/main.go @@ -57,7 +57,7 @@ func main() { var err error certService, err = cert.New( config.Grpc.TLS.CAFile, - "./certs/ca/ca-key.pem", + config.Grpc.TLS.CAKeyFile, config.Grpc.TLS.CertFile, config.Grpc.TLS.KeyFile, certOpts, From 489d81d83d5a7f58adc264b6c85037ea5d8b8ebb Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:27:27 +0800 Subject: [PATCH 09/29] bug fix on the CA key loading for the private key format --- internal/cert/service.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/cert/service.go b/internal/cert/service.go index 77d77cd..53c9782 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -263,11 +263,16 @@ func loadCA(certPath, keyPath string) (*x509.Certificate, *rsa.PrivateKey, error return nil, nil, fmt.Errorf("failed to decode CA key PEM") } - caKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes) if err != nil { return nil, nil, fmt.Errorf("failed to parse CA key: %w", err) } + caKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, nil, fmt.Errorf("CA key is not an RSA private key") + } + return caCert, caKey, nil } @@ -295,9 +300,14 @@ func writeKeyToFile(key *rsa.PrivateKey, path string) error { } defer keyFile.Close() + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return fmt.Errorf("failed to marshal key: %w", err) + } + if err := pem.Encode(keyFile, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(key), + Type: "PRIVATE KEY", + Bytes: keyBytes, }); err != nil { return fmt.Errorf("failed to encode key: %w", err) } From 2fdfa85a77f4959c6ab0994c335ffa7ce1746f7d Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:44:43 +0800 Subject: [PATCH 10/29] refactor the cert layer --- internal/cert/generate.go | 142 +++++++++++++++++++++++++ internal/cert/service.go | 217 -------------------------------------- internal/cert/utils.go | 92 ++++++++++++++++ 3 files changed, 234 insertions(+), 217 deletions(-) create mode 100644 internal/cert/generate.go create mode 100644 internal/cert/utils.go diff --git a/internal/cert/generate.go b/internal/cert/generate.go new file mode 100644 index 0000000..170c86a --- /dev/null +++ b/internal/cert/generate.go @@ -0,0 +1,142 @@ +package cert + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "log/slog" + "math/big" + "net" + "time" +) + +func generateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey, domainNames []string, ipAddresses []net.IP) (*x509.Certificate, *rsa.PrivateKey, error) { + serverKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate server key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) + } + + commonName := "localhost" + if len(domainNames) > 0 { + commonName = domainNames[0] + } + + serverTemplate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Silo Proxy"}, + CommonName: commonName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: domainNames, + IPAddresses: ipAddresses, + } + + serverCertBytes, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create server certificate: %w", err) + } + + serverCert, err := x509.ParseCertificate(serverCertBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse server certificate: %w", err) + } + + return serverCert, serverKey, nil +} + +func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) { + caKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate CA key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) + } + + caTemplate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Silo Proxy CA"}, + CommonName: "Silo Proxy Root CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + + caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create CA certificate: %w", err) + } + + caCert, err := x509.ParseCertificate(caCertBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err) + } + + return caCert, caKey, nil +} + +func (s *Service) GenerateAgentCert(agentID string) (*x509.Certificate, *rsa.PrivateKey, error) { + slog.Info("Generating agent certificate", "agent_id", agentID) + + caCert, caKey, err := loadCA(s.CaCertPath, s.CaKeyPath) + if err != nil { + slog.Error("Failed to load CA for agent cert generation", "error", err, "agent_id", agentID) + return nil, nil, fmt.Errorf("failed to load CA: %w", err) + } + + agentKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate agent key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) + } + + agentTemplate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Silo Proxy"}, + CommonName: agentID, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + agentCertBytes, err := x509.CreateCertificate(rand.Reader, agentTemplate, caCert, &agentKey.PublicKey, caKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create agent certificate: %w", err) + } + + agentCert, err := x509.ParseCertificate(agentCertBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse agent certificate: %w", err) + } + + slog.Info("Generated agent certificate", "agent_id", agentID) + return agentCert, agentKey, nil +} diff --git a/internal/cert/service.go b/internal/cert/service.go index 53c9782..5d32aa0 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -1,18 +1,13 @@ package cert import ( - "crypto/rand" "crypto/rsa" "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" "fmt" "log/slog" - "math/big" "net" "os" "path/filepath" - "time" ) type Service struct { @@ -154,218 +149,6 @@ func (s *Service) ensureDirectory(filePath string) error { return nil } -func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) { - caKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate CA key: %w", err) - } - - serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) - } - - caTemplate := &x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{"Silo Proxy CA"}, - CommonName: "Silo Proxy Root CA", - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, - BasicConstraintsValid: true, - IsCA: true, - MaxPathLenZero: true, - } - - caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) - if err != nil { - return nil, nil, fmt.Errorf("failed to create CA certificate: %w", err) - } - - caCert, err := x509.ParseCertificate(caCertBytes) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err) - } - - return caCert, caKey, nil -} - -func generateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey, domainNames []string, ipAddresses []net.IP) (*x509.Certificate, *rsa.PrivateKey, error) { - serverKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate server key: %w", err) - } - - serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) - } - - commonName := "localhost" - if len(domainNames) > 0 { - commonName = domainNames[0] - } - - serverTemplate := &x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{"Silo Proxy"}, - CommonName: commonName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - DNSNames: domainNames, - IPAddresses: ipAddresses, - } - - serverCertBytes, err := x509.CreateCertificate(rand.Reader, serverTemplate, caCert, &serverKey.PublicKey, caKey) - if err != nil { - return nil, nil, fmt.Errorf("failed to create server certificate: %w", err) - } - - serverCert, err := x509.ParseCertificate(serverCertBytes) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse server certificate: %w", err) - } - - return serverCert, serverKey, nil -} - -func loadCA(certPath, keyPath string) (*x509.Certificate, *rsa.PrivateKey, error) { - certBytes, err := os.ReadFile(certPath) - if err != nil { - return nil, nil, fmt.Errorf("failed to read CA certificate: %w", err) - } - - certBlock, _ := pem.Decode(certBytes) - if certBlock == nil { - return nil, nil, fmt.Errorf("failed to decode CA certificate PEM") - } - - caCert, err := x509.ParseCertificate(certBlock.Bytes) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err) - } - - keyBytes, err := os.ReadFile(keyPath) - if err != nil { - return nil, nil, fmt.Errorf("failed to read CA key: %w", err) - } - - keyBlock, _ := pem.Decode(keyBytes) - if keyBlock == nil { - return nil, nil, fmt.Errorf("failed to decode CA key PEM") - } - - key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse CA key: %w", err) - } - - caKey, ok := key.(*rsa.PrivateKey) - if !ok { - return nil, nil, fmt.Errorf("CA key is not an RSA private key") - } - - return caCert, caKey, nil -} - -func writeCertToFile(cert *x509.Certificate, path string) error { - certFile, err := os.Create(path) - if err != nil { - return fmt.Errorf("failed to create certificate file: %w", err) - } - defer certFile.Close() - - if err := pem.Encode(certFile, &pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }); err != nil { - return fmt.Errorf("failed to encode certificate: %w", err) - } - - return nil -} - -func writeKeyToFile(key *rsa.PrivateKey, path string) error { - keyFile, err := os.Create(path) - if err != nil { - return fmt.Errorf("failed to create key file: %w", err) - } - defer keyFile.Close() - - keyBytes, err := x509.MarshalPKCS8PrivateKey(key) - if err != nil { - return fmt.Errorf("failed to marshal key: %w", err) - } - - if err := pem.Encode(keyFile, &pem.Block{ - Type: "PRIVATE KEY", - Bytes: keyBytes, - }); err != nil { - return fmt.Errorf("failed to encode key: %w", err) - } - - return nil -} - -func fileExists(path string) bool { - _, err := os.Stat(path) - return err == nil -} - -func (s *Service) GenerateAgentCert(agentID string) (*x509.Certificate, *rsa.PrivateKey, error) { - slog.Info("Generating agent certificate", "agent_id", agentID) - - caCert, caKey, err := loadCA(s.CaCertPath, s.CaKeyPath) - if err != nil { - slog.Error("Failed to load CA for agent cert generation", "error", err, "agent_id", agentID) - return nil, nil, fmt.Errorf("failed to load CA: %w", err) - } - - agentKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate agent key: %w", err) - } - - serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) - } - - agentTemplate := &x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{"Silo Proxy"}, - CommonName: agentID, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - BasicConstraintsValid: true, - } - - agentCertBytes, err := x509.CreateCertificate(rand.Reader, agentTemplate, caCert, &agentKey.PublicKey, caKey) - if err != nil { - return nil, nil, fmt.Errorf("failed to create agent certificate: %w", err) - } - - agentCert, err := x509.ParseCertificate(agentCertBytes) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse agent certificate: %w", err) - } - - slog.Info("Generated agent certificate", "agent_id", agentID) - return agentCert, agentKey, nil -} - func (s *Service) GetCACert() ([]byte, error) { certBytes, err := os.ReadFile(s.CaCertPath) if err != nil { diff --git a/internal/cert/utils.go b/internal/cert/utils.go new file mode 100644 index 0000000..cf4d574 --- /dev/null +++ b/internal/cert/utils.go @@ -0,0 +1,92 @@ +package cert + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" +) + +func loadCA(certPath, keyPath string) (*x509.Certificate, *rsa.PrivateKey, error) { + certBytes, err := os.ReadFile(certPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to read CA certificate: %w", err) + } + + certBlock, _ := pem.Decode(certBytes) + if certBlock == nil { + return nil, nil, fmt.Errorf("failed to decode CA certificate PEM") + } + + caCert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err) + } + + keyBytes, err := os.ReadFile(keyPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to read CA key: %w", err) + } + + keyBlock, _ := pem.Decode(keyBytes) + if keyBlock == nil { + return nil, nil, fmt.Errorf("failed to decode CA key PEM") + } + + key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse CA key: %w", err) + } + + caKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, nil, fmt.Errorf("CA key is not an RSA private key") + } + + return caCert, caKey, nil +} + +func writeCertToFile(cert *x509.Certificate, path string) error { + certFile, err := os.Create(path) + if err != nil { + return fmt.Errorf("failed to create certificate file: %w", err) + } + defer certFile.Close() + + if err := pem.Encode(certFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }); err != nil { + return fmt.Errorf("failed to encode certificate: %w", err) + } + + return nil +} + +func writeKeyToFile(key *rsa.PrivateKey, path string) error { + keyFile, err := os.Create(path) + if err != nil { + return fmt.Errorf("failed to create key file: %w", err) + } + defer keyFile.Close() + + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return fmt.Errorf("failed to marshal key: %w", err) + } + + if err := pem.Encode(keyFile, &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + }); err != nil { + return fmt.Errorf("failed to encode key: %w", err) + } + + return nil +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} From 22c5f03848692be6ec50f9e9cdb33b64dcfec490 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:45:34 +0800 Subject: [PATCH 11/29] update GenerateServerCert signature --- internal/cert/generate.go | 2 +- internal/cert/service.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cert/generate.go b/internal/cert/generate.go index 170c86a..d107c62 100644 --- a/internal/cert/generate.go +++ b/internal/cert/generate.go @@ -12,7 +12,7 @@ import ( "time" ) -func generateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey, domainNames []string, ipAddresses []net.IP) (*x509.Certificate, *rsa.PrivateKey, error) { +func (s *Service) GenerateServerCert(caCert *x509.Certificate, caKey *rsa.PrivateKey, domainNames []string, ipAddresses []net.IP) (*x509.Certificate, *rsa.PrivateKey, error) { serverKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, nil, fmt.Errorf("failed to generate server key: %w", err) diff --git a/internal/cert/service.go b/internal/cert/service.go index 5d32aa0..842d2a9 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -108,7 +108,7 @@ func (s *Service) ensureCertificates() error { "domains", s.DomainNames, "ips", s.IPAddresses) - serverCert, serverKey, err := generateServerCert(caCert, caKey, s.DomainNames, s.IPAddresses) + serverCert, serverKey, err := s.GenerateServerCert(caCert, caKey, s.DomainNames, s.IPAddresses) if err != nil { slog.Error("Failed to generate server certificate", "error", err) return fmt.Errorf("failed to generate server certificate: %w", err) From 06bfe85c493eca01dc5343b6d1ddd4542f4afeb0 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:46:25 +0800 Subject: [PATCH 12/29] update the Generate CA function --- internal/cert/generate.go | 2 +- internal/cert/service.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cert/generate.go b/internal/cert/generate.go index d107c62..aae02d6 100644 --- a/internal/cert/generate.go +++ b/internal/cert/generate.go @@ -56,7 +56,7 @@ func (s *Service) GenerateServerCert(caCert *x509.Certificate, caKey *rsa.Privat return serverCert, serverKey, nil } -func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) { +func (s *Service) GenerateCA() (*x509.Certificate, *rsa.PrivateKey, error) { caKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, nil, fmt.Errorf("failed to generate CA key: %w", err) diff --git a/internal/cert/service.go b/internal/cert/service.go index 842d2a9..58822d8 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -63,7 +63,7 @@ func (s *Service) ensureCertificates() error { slog.Info("CA certificate not found, generating new CA", "cert_path", s.CaCertPath) var err error - caCert, caKey, err = generateCA() + caCert, caKey, err = s.GenerateCA() if err != nil { slog.Error("Failed to generate CA certificate", "error", err) return fmt.Errorf("failed to generate CA certificate: %w", err) From 94ad9f21a1690aacd62758e6ffd22c70def87c2e Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:52:55 +0800 Subject: [PATCH 13/29] Clean up the cert KeyToPEM function to utils --- internal/api/http/handler/cert.go | 29 ++-------------- internal/cert/utils.go | 55 ++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 4e29296..2bb6641 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -3,9 +3,6 @@ package handler import ( "archive/zip" "bytes" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" "log/slog" "net/http" @@ -72,7 +69,7 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { zipBuffer := new(bytes.Buffer) zipWriter := zip.NewWriter(zipBuffer) - agentCertPEM, err := certToPEM(agentCert) + agentCertPEM, err := cert.CertToPEM(agentCert) if err != nil { slog.Error("Failed to encode agent certificate", "error", err, "agent_id", req.AgentID) ctx.JSON(http.StatusInternalServerError, gin.H{ @@ -81,7 +78,7 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { return } - agentKeyPEM, err := keyToPEM(agentKey) + agentKeyPEM, err := cert.KeyToPEM(agentKey) if err != nil { slog.Error("Failed to encode agent key", "error", err, "agent_id", req.AgentID) ctx.JSON(http.StatusInternalServerError, gin.H{ @@ -128,25 +125,3 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { slog.Info("Agent certificates provisioned successfully", "agent_id", req.AgentID, "zip_size", zipBuffer.Len()) } - -func certToPEM(cert *x509.Certificate) ([]byte, error) { - var buf bytes.Buffer - if err := pem.Encode(&buf, &pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func keyToPEM(key *rsa.PrivateKey) ([]byte, error) { - var buf bytes.Buffer - if err := pem.Encode(&buf, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(key), - }); err != nil { - return nil, err - } - return buf.Bytes(), nil -} diff --git a/internal/cert/utils.go b/internal/cert/utils.go index cf4d574..2032387 100644 --- a/internal/cert/utils.go +++ b/internal/cert/utils.go @@ -1,6 +1,7 @@ package cert import ( + "bytes" "crypto/rsa" "crypto/x509" "encoding/pem" @@ -47,42 +48,56 @@ func loadCA(certPath, keyPath string) (*x509.Certificate, *rsa.PrivateKey, error return caCert, caKey, nil } -func writeCertToFile(cert *x509.Certificate, path string) error { - certFile, err := os.Create(path) - if err != nil { - return fmt.Errorf("failed to create certificate file: %w", err) - } - defer certFile.Close() - - if err := pem.Encode(certFile, &pem.Block{ +func CertToPEM(cert *x509.Certificate) ([]byte, error) { + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, }); err != nil { - return fmt.Errorf("failed to encode certificate: %w", err) + return nil, err } - - return nil + return buf.Bytes(), nil } -func writeKeyToFile(key *rsa.PrivateKey, path string) error { - keyFile, err := os.Create(path) - if err != nil { - return fmt.Errorf("failed to create key file: %w", err) - } - defer keyFile.Close() - +func KeyToPEM(key *rsa.PrivateKey) ([]byte, error) { keyBytes, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { - return fmt.Errorf("failed to marshal key: %w", err) + return nil, err } - if err := pem.Encode(keyFile, &pem.Block{ + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{ Type: "PRIVATE KEY", Bytes: keyBytes, }); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func writeCertToFile(cert *x509.Certificate, path string) error { + pemBytes, err := CertToPEM(cert) + if err != nil { + return fmt.Errorf("failed to encode certificate: %w", err) + } + + if err := os.WriteFile(path, pemBytes, 0644); err != nil { + return fmt.Errorf("failed to write certificate file: %w", err) + } + + return nil +} + +func writeKeyToFile(key *rsa.PrivateKey, path string) error { + pemBytes, err := KeyToPEM(key) + if err != nil { return fmt.Errorf("failed to encode key: %w", err) } + if err := os.WriteFile(path, pemBytes, 0600); err != nil { + return fmt.Errorf("failed to write key file: %w", err) + } + return nil } From c7fe2182ea45835588ce1f1f2b26841608a38e4c Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:58:03 +0800 Subject: [PATCH 14/29] Add Clear all server certs API --- internal/api/http/handler/cert.go | 54 +++++++++++++++++++++++++++++++ internal/api/http/router.go | 1 + 2 files changed, 55 insertions(+) diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 2bb6641..2ca8c42 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net/http" + "os" "github.com/EternisAI/silo-proxy/internal/api/http/dto" "github.com/EternisAI/silo-proxy/internal/cert" @@ -125,3 +126,56 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { slog.Info("Agent certificates provisioned successfully", "agent_id", req.AgentID, "zip_size", zipBuffer.Len()) } + +func (h *CertHandler) DeleteServerCerts(ctx *gin.Context) { + if h.certService == nil { + slog.Warn("Server cert deletion requested but TLS is disabled") + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "TLS is not enabled on this server", + }) + return + } + + slog.Info("Deleting all server certificates") + + certPaths := []string{ + h.certService.CaCertPath, + h.certService.CaKeyPath, + h.certService.ServerCertPath, + h.certService.ServerKeyPath, + } + + deletedFiles := []string{} + var errors []string + + for _, path := range certPaths { + if _, err := os.Stat(path); os.IsNotExist(err) { + slog.Debug("Certificate file does not exist, skipping", "path", path) + continue + } + + if err := os.Remove(path); err != nil { + slog.Error("Failed to delete certificate file", "error", err, "path", path) + errors = append(errors, fmt.Sprintf("%s: %v", path, err)) + } else { + slog.Info("Deleted certificate file", "path", path) + deletedFiles = append(deletedFiles, path) + } + } + + if len(errors) > 0 { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete some certificate files", + "deleted_files": deletedFiles, + "errors": errors, + }) + return + } + + ctx.JSON(http.StatusOK, gin.H{ + "message": "Successfully deleted server certificates", + "deleted_files": deletedFiles, + }) + + slog.Info("Server certificates deleted successfully", "count", len(deletedFiles)) +} diff --git a/internal/api/http/router.go b/internal/api/http/router.go index fa3c291..1721a55 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -26,4 +26,5 @@ func SetupRoute(engine *gin.Engine, srvs *Services) { certHandler := handler.NewCertHandler(srvs.CertService) engine.POST("/cert/agent", certHandler.ProvisionAgent) + engine.DELETE("/cert/server", certHandler.DeleteServerCerts) } From 57338e774c8580dcb0559ff2bc0dd6eacc927b27 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:58:09 +0800 Subject: [PATCH 15/29] Add simple curl --- misc/delete-server-certs.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 misc/delete-server-certs.sh diff --git a/misc/delete-server-certs.sh b/misc/delete-server-certs.sh new file mode 100644 index 0000000..0d359b0 --- /dev/null +++ b/misc/delete-server-certs.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X DELETE http://localhost:8080/cert/server From e3fa4f234609f5a03dfeff25c3831b3e0c778225 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 01:58:19 +0800 Subject: [PATCH 16/29] Add simple curl --- misc/delete-server-certs.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 misc/delete-server-certs.sh diff --git a/misc/delete-server-certs.sh b/misc/delete-server-certs.sh old mode 100644 new mode 100755 From 6c1dfe95f6ce72e3f3ccca24687d241b37643326 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 02:10:58 +0800 Subject: [PATCH 17/29] simplify defer for zipwriter close --- internal/api/http/handler/cert.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 2ca8c42..414e539 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -69,6 +69,7 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { zipBuffer := new(bytes.Buffer) zipWriter := zip.NewWriter(zipBuffer) + defer zipWriter.Close() agentCertPEM, err := cert.CertToPEM(agentCert) if err != nil { @@ -112,14 +113,6 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { } } - if err := zipWriter.Close(); err != nil { - slog.Error("Failed to close zip writer", "error", err) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to finalize zip file", - }) - return - } - ctx.Header("Content-Type", "application/zip") ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-certs.zip\"", req.AgentID)) ctx.Data(http.StatusOK, "application/zip", zipBuffer.Bytes()) From 7759bbbf40308f16e4c6835ea73aeaa19e09617c Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 02:12:27 +0800 Subject: [PATCH 18/29] bug fix on zip writer for agent generate --- internal/api/http/handler/cert.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 414e539..2ca8c42 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -69,7 +69,6 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { zipBuffer := new(bytes.Buffer) zipWriter := zip.NewWriter(zipBuffer) - defer zipWriter.Close() agentCertPEM, err := cert.CertToPEM(agentCert) if err != nil { @@ -113,6 +112,14 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { } } + if err := zipWriter.Close(); err != nil { + slog.Error("Failed to close zip writer", "error", err) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to finalize zip file", + }) + return + } + ctx.Header("Content-Type", "application/zip") ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-certs.zip\"", req.AgentID)) ctx.Data(http.StatusOK, "application/zip", zipBuffer.Bytes()) From 0631367c3e149b2c8e242d6640c3098663ccf662 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 12:27:58 +0800 Subject: [PATCH 19/29] Update domainnames config handling --- cmd/silo-proxy-server/config.go | 14 -------------- cmd/silo-proxy-server/main.go | 21 ++------------------- internal/cert/service.go | 28 +++++++++++++--------------- internal/cert/utils.go | 15 +++++++++++++++ 4 files changed, 30 insertions(+), 48 deletions(-) diff --git a/cmd/silo-proxy-server/config.go b/cmd/silo-proxy-server/config.go index 4bfb911..d40e27d 100644 --- a/cmd/silo-proxy-server/config.go +++ b/cmd/silo-proxy-server/config.go @@ -34,20 +34,6 @@ type TLSConfig struct { var config Config -func ParseCommaSeparated(input string) []string { - if input == "" { - return nil - } - parts := strings.Split(input, ",") - result := make([]string, 0, len(parts)) - for _, part := range parts { - if trimmed := strings.TrimSpace(part); trimmed != "" { - result = append(result, trimmed) - } - } - return result -} - func InitConfig() { var err error diff --git a/cmd/silo-proxy-server/main.go b/cmd/silo-proxy-server/main.go index 65b35a1..e34e80b 100644 --- a/cmd/silo-proxy-server/main.go +++ b/cmd/silo-proxy-server/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log/slog" - "net" "net/http" "os" "os/signal" @@ -36,23 +35,6 @@ func main() { var certService *cert.Service if config.Grpc.TLS.Enabled { - certOpts := &cert.Options{} - - domainNames := ParseCommaSeparated(config.Grpc.TLS.DomainNames) - if len(domainNames) > 0 { - certOpts.DomainNames = domainNames - } - - ipAddresses := ParseCommaSeparated(config.Grpc.TLS.IPAddresses) - if len(ipAddresses) > 0 { - for _, ipStr := range ipAddresses { - if ip := net.ParseIP(ipStr); ip != nil { - certOpts.IPAddresses = append(certOpts.IPAddresses, ip) - } else { - slog.Warn("Invalid IP address in configuration, skipping", "ip", ipStr) - } - } - } var err error certService, err = cert.New( @@ -60,7 +42,8 @@ func main() { config.Grpc.TLS.CAKeyFile, config.Grpc.TLS.CertFile, config.Grpc.TLS.KeyFile, - certOpts, + config.Grpc.TLS.DomainNames, + config.Grpc.TLS.IPAddresses, ) if err != nil { slog.Error("Failed to initialize certificates", "error", err) diff --git a/internal/cert/service.go b/internal/cert/service.go index 58822d8..db281c4 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -19,12 +19,7 @@ type Service struct { IPAddresses []net.IP } -type Options struct { - DomainNames []string - IPAddresses []net.IP -} - -func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath string, opts *Options) (*Service, error) { +func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath, domainNamesConfig, IPAddressesConfig string) (*Service, error) { s := &Service{ CaCertPath: caCertPath, CaKeyPath: caKeyPath, @@ -32,17 +27,20 @@ func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath string, opts *Opti ServerKeyPath: serverKeyPath, } - if opts != nil { - s.DomainNames = opts.DomainNames - s.IPAddresses = opts.IPAddresses + domainNames := ParseCommaSeparated(domainNamesConfig) + if len(domainNames) > 0 { + s.DomainNames = domainNames } - if len(s.DomainNames) == 0 { - s.DomainNames = []string{"localhost"} - } - - if len(s.IPAddresses) == 0 { - s.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} + ipAddresses := ParseCommaSeparated(IPAddressesConfig) + if len(ipAddresses) > 0 { + for _, ipStr := range ipAddresses { + if ip := net.ParseIP(ipStr); ip != nil { + s.IPAddresses = append(s.IPAddresses, ip) + } else { + slog.Warn("Invalid IP address in configuration, skipping", "ip", ipStr) + } + } } if err := s.ensureCertificates(); err != nil { diff --git a/internal/cert/utils.go b/internal/cert/utils.go index 2032387..957b17d 100644 --- a/internal/cert/utils.go +++ b/internal/cert/utils.go @@ -7,8 +7,23 @@ import ( "encoding/pem" "fmt" "os" + "strings" ) +func ParseCommaSeparated(input string) []string { + if input == "" { + return nil + } + parts := strings.Split(input, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + func loadCA(certPath, keyPath string) (*x509.Certificate, *rsa.PrivateKey, error) { certBytes, err := os.ReadFile(certPath) if err != nil { From f08a6fda1507eb62f0be5f16646c86adc7f27fc7 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 12:48:27 +0800 Subject: [PATCH 20/29] Update API spec --- cmd/silo-proxy-server/application.yml | 1 + cmd/silo-proxy-server/config.go | 17 +- cmd/silo-proxy-server/main.go | 1 + internal/api/http/dto/cert.go | 19 ++- internal/api/http/handler/cert.go | 232 +++++++++++++++++++++++--- internal/api/http/router.go | 23 ++- internal/cert/generate.go | 19 ++- internal/cert/service.go | 77 ++++++++- 8 files changed, 346 insertions(+), 43 deletions(-) diff --git a/cmd/silo-proxy-server/application.yml b/cmd/silo-proxy-server/application.yml index d737f9c..e1c049b 100644 --- a/cmd/silo-proxy-server/application.yml +++ b/cmd/silo-proxy-server/application.yml @@ -16,3 +16,4 @@ grpc: client_auth: require domain_names: "localhost" ip_addresses: "127.0.0.1" + agent_cert_dir: ./certs/agents diff --git a/cmd/silo-proxy-server/config.go b/cmd/silo-proxy-server/config.go index d40e27d..e4d88f3 100644 --- a/cmd/silo-proxy-server/config.go +++ b/cmd/silo-proxy-server/config.go @@ -22,14 +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"` - CAKeyFile string `mapstructure:"ca_key_file"` - ClientAuth string `mapstructure:"client_auth"` - DomainNames string `mapstructure:"domain_names"` - IPAddresses string `mapstructure:"ip_addresses"` + 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 diff --git a/cmd/silo-proxy-server/main.go b/cmd/silo-proxy-server/main.go index e34e80b..b488cfc 100644 --- a/cmd/silo-proxy-server/main.go +++ b/cmd/silo-proxy-server/main.go @@ -42,6 +42,7 @@ func main() { config.Grpc.TLS.CAKeyFile, config.Grpc.TLS.CertFile, config.Grpc.TLS.KeyFile, + config.Grpc.TLS.AgentCertDir, config.Grpc.TLS.DomainNames, config.Grpc.TLS.IPAddresses, ) diff --git a/internal/api/http/dto/cert.go b/internal/api/http/dto/cert.go index 3f93954..4446c91 100644 --- a/internal/api/http/dto/cert.go +++ b/internal/api/http/dto/cert.go @@ -1,5 +1,20 @@ package dto -type ProvisionAgentRequest struct { - AgentID string `json:"agent_id" binding:"required"` +import "time" + +type AgentCertInfo struct { + AgentID string `json:"agent_id"` + CreatedAt time.Time `json:"created_at"` + ExpiresAt time.Time `json:"expires_at"` + CertPath string `json:"cert_path"` +} + +type ListAgentsResponse struct { + Agents []AgentCertInfo `json:"agents"` + Count int `json:"count"` +} + +type DeleteCertificateResponse struct { + Message string `json:"message"` + DeletedPaths []string `json:"deleted_paths,omitempty"` } diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 2ca8c42..807a10e 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -3,6 +3,9 @@ package handler import ( "archive/zip" "bytes" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "log/slog" "net/http" @@ -23,35 +26,35 @@ func NewCertHandler(certService *cert.Service) *CertHandler { } } -func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { +func (h *CertHandler) CreateAgentCertificate(ctx *gin.Context) { if h.certService == nil { - slog.Warn("Agent cert provisioning requested but TLS is disabled") + 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 } - var req dto.ProvisionAgentRequest - if err := ctx.ShouldBindJSON(&req); err != nil { + agentID := ctx.Param("id") + if agentID == "" { ctx.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Invalid request: %v", err), + "error": "agent_id is required", }) return } - if req.AgentID == "" { - ctx.JSON(http.StatusBadRequest, gin.H{ - "error": "agent_id is required", + if h.certService.AgentCertExists(agentID) { + ctx.JSON(http.StatusConflict, gin.H{ + "error": "Certificate already exists for this agent", }) return } - slog.Info("Provisioning agent certificates", "agent_id", req.AgentID) + slog.Info("Creating agent certificate", "agent_id", agentID) - agentCert, agentKey, err := h.certService.GenerateAgentCert(req.AgentID) + agentCert, agentKey, err := h.certService.GenerateAgentCert(agentID) if err != nil { - slog.Error("Failed to generate agent certificate", "error", err, "agent_id", req.AgentID) + slog.Error("Failed to generate agent certificate", "error", err, "agent_id", agentID) ctx.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to generate agent certificate", }) @@ -60,38 +63,80 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { caCertBytes, err := h.certService.GetCACert() if err != nil { - slog.Error("Failed to read CA certificate", "error", err, "agent_id", req.AgentID) + 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 := new(bytes.Buffer) - zipWriter := zip.NewWriter(zipBuffer) + 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 + } - agentCertPEM, err := cert.CertToPEM(agentCert) + 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 := ctx.Param("id") + if agentID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "agent_id is required", + }) + return + } + + if !h.certService.AgentCertExists(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 encode agent certificate", "error", err, "agent_id", req.AgentID) + slog.Error("Failed to read agent certificate", "error", err, "agent_id", agentID) ctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to encode agent certificate", + "error": "Failed to read agent certificate", }) return } - agentKeyPEM, err := cert.KeyToPEM(agentKey) + caCertBytes, err := h.certService.GetCACert() if err != nil { - slog.Error("Failed to encode agent key", "error", err, "agent_id", req.AgentID) + slog.Error("Failed to read CA certificate", "error", err, "agent_id", agentID) ctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to encode agent key", + "error": "Failed to read CA certificate", }) return } + zipBuffer := new(bytes.Buffer) + zipWriter := zip.NewWriter(zipBuffer) + files := map[string][]byte{ - fmt.Sprintf("%s-cert.pem", req.AgentID): agentCertPEM, - fmt.Sprintf("%s-key.pem", req.AgentID): agentKeyPEM, - "ca-cert.pem": caCertBytes, + fmt.Sprintf("%s-cert.pem", agentID): agentCertBytes, + fmt.Sprintf("%s-key.pem", agentID): agentKeyBytes, + "ca-cert.pem": caCertBytes, } for filename, content := range files { @@ -121,10 +166,147 @@ func (h *CertHandler) ProvisionAgent(ctx *gin.Context) { } ctx.Header("Content-Type", "application/zip") - ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-certs.zip\"", req.AgentID)) + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-certs.zip\"", agentID)) ctx.Data(http.StatusOK, "application/zip", zipBuffer.Bytes()) - slog.Info("Agent certificates provisioned successfully", "agent_id", req.AgentID, "zip_size", zipBuffer.Len()) + slog.Info("Agent certificate retrieved successfully", "agent_id", agentID, "zip_size", zipBuffer.Len()) +} + +func (h *CertHandler) ListAgents(ctx *gin.Context) { + if h.certService == nil { + slog.Warn("Agent list requested but TLS is disabled") + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "TLS is not enabled on this server", + }) + return + } + + agentIDs, err := h.certService.ListAgentCerts() + if err != nil { + slog.Error("Failed to list agent certificates", "error", err) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list agent certificates", + }) + return + } + + agents := make([]dto.AgentCertInfo, 0, len(agentIDs)) + for _, agentID := range agentIDs { + certPath := h.certService.GetAgentCertPath(agentID) + agentCertBytes, _, err := h.certService.GetAgentCert(agentID) + if err != nil { + slog.Warn("Failed to read agent certificate for listing", "error", err, "agent_id", agentID) + continue + } + + block, _ := pem.Decode(agentCertBytes) + if block == nil { + slog.Warn("Failed to decode PEM certificate", "agent_id", agentID) + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + slog.Warn("Failed to parse certificate", "error", err, "agent_id", agentID) + continue + } + + agents = append(agents, dto.AgentCertInfo{ + AgentID: agentID, + CreatedAt: cert.NotBefore, + ExpiresAt: cert.NotAfter, + CertPath: certPath, + }) + } + + response := dto.ListAgentsResponse{ + Agents: agents, + Count: len(agents), + } + + ctx.JSON(http.StatusOK, response) + slog.Info("Listed agent certificates", "count", len(agents)) +} + +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 := ctx.Param("id") + if agentID == "" { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "agent_id is required", + }) + return + } + + if !h.certService.AgentCertExists(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 + } + + response := dto.DeleteCertificateResponse{ + Message: "Successfully deleted agent certificate", + DeletedPaths: []string{certDir}, + } + + ctx.JSON(http.StatusOK, response) + 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) + } + + zipBuffer := new(bytes.Buffer) + zipWriter := zip.NewWriter(zipBuffer) + + files := map[string][]byte{ + fmt.Sprintf("%s-cert.pem", agentID): agentCertPEM, + fmt.Sprintf("%s-key.pem", agentID): agentKeyPEM, + "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 close zip writer: %w", err) + } + + return zipBuffer, nil } func (h *CertHandler) DeleteServerCerts(ctx *gin.Context) { diff --git a/internal/api/http/router.go b/internal/api/http/router.go index 1721a55..a41791e 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -19,12 +19,23 @@ func SetupRoute(engine *gin.Engine, srvs *Services) { healthHandler := handler.NewHealthHandler() engine.GET("/health", healthHandler.Check) - if srvs.GrpcServer != nil { - adminHandler := handler.NewAdminHandler(srvs.GrpcServer) - engine.GET("/agents", adminHandler.ListAgents) + certHandler := handler.NewCertHandler(srvs.CertService) + + agents := engine.Group("/agents") + { + if srvs.GrpcServer != nil { + adminHandler := handler.NewAdminHandler(srvs.GrpcServer) + agents.GET("", adminHandler.ListAgents) + } + + agents.POST("/:id/certificate", certHandler.CreateAgentCertificate) + agents.GET("/:id/certificate", certHandler.GetAgentCertificate) + agents.DELETE("/:id/certificate", certHandler.DeleteAgentCertificate) } - certHandler := handler.NewCertHandler(srvs.CertService) - engine.POST("/cert/agent", certHandler.ProvisionAgent) - engine.DELETE("/cert/server", certHandler.DeleteServerCerts) + cert := engine.Group("/cert") + { + cert.GET("/agents", certHandler.ListAgents) + cert.DELETE("/server", certHandler.DeleteServerCerts) + } } diff --git a/internal/cert/generate.go b/internal/cert/generate.go index aae02d6..51ce1f8 100644 --- a/internal/cert/generate.go +++ b/internal/cert/generate.go @@ -137,6 +137,23 @@ func (s *Service) GenerateAgentCert(agentID string) (*x509.Certificate, *rsa.Pri return nil, nil, fmt.Errorf("failed to parse agent certificate: %w", err) } - slog.Info("Generated agent certificate", "agent_id", agentID) + certPath := s.GetAgentCertPath(agentID) + keyPath := s.GetAgentKeyPath(agentID) + + if err := s.ensureDirectory(certPath); err != nil { + return nil, nil, fmt.Errorf("failed to create agent cert directory: %w", err) + } + + if err := writeCertToFile(agentCert, certPath); err != nil { + slog.Error("Failed to write agent certificate", "error", err, "path", certPath) + return nil, nil, fmt.Errorf("failed to write agent certificate: %w", err) + } + + if err := writeKeyToFile(agentKey, keyPath); err != nil { + slog.Error("Failed to write agent key", "error", err, "path", keyPath) + return nil, nil, fmt.Errorf("failed to write agent key: %w", err) + } + + slog.Info("Generated and saved agent certificate", "agent_id", agentID, "cert_path", certPath, "key_path", keyPath) return agentCert, agentKey, nil } diff --git a/internal/cert/service.go b/internal/cert/service.go index db281c4..0cdbe21 100644 --- a/internal/cert/service.go +++ b/internal/cert/service.go @@ -15,16 +15,18 @@ type Service struct { CaKeyPath string ServerCertPath string ServerKeyPath string + AgentCertDir string DomainNames []string IPAddresses []net.IP } -func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath, domainNamesConfig, IPAddressesConfig string) (*Service, error) { +func New(caCertPath, caKeyPath, serverCertPath, serverKeyPath, agentCertDir, domainNamesConfig, IPAddressesConfig string) (*Service, error) { s := &Service{ CaCertPath: caCertPath, CaKeyPath: caKeyPath, ServerCertPath: serverCertPath, ServerKeyPath: serverKeyPath, + AgentCertDir: agentCertDir, } domainNames := ParseCommaSeparated(domainNamesConfig) @@ -154,3 +156,76 @@ func (s *Service) GetCACert() ([]byte, error) { } return certBytes, nil } + +func (s *Service) GetAgentCertDir(agentID string) string { + return filepath.Join(s.AgentCertDir, agentID) +} + +func (s *Service) GetAgentCertPath(agentID string) string { + return filepath.Join(s.GetAgentCertDir(agentID), fmt.Sprintf("%s-cert.pem", agentID)) +} + +func (s *Service) GetAgentKeyPath(agentID string) string { + return filepath.Join(s.GetAgentCertDir(agentID), fmt.Sprintf("%s-key.pem", agentID)) +} + +func (s *Service) AgentCertExists(agentID string) bool { + certPath := s.GetAgentCertPath(agentID) + keyPath := s.GetAgentKeyPath(agentID) + return fileExists(certPath) && fileExists(keyPath) +} + +func (s *Service) GetAgentCert(agentID string) (certBytes, keyBytes []byte, err error) { + certPath := s.GetAgentCertPath(agentID) + keyPath := s.GetAgentKeyPath(agentID) + + certBytes, err = os.ReadFile(certPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to read agent certificate: %w", err) + } + + keyBytes, err = os.ReadFile(keyPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to read agent key: %w", err) + } + + return certBytes, keyBytes, nil +} + +func (s *Service) DeleteAgentCert(agentID string) error { + certDir := s.GetAgentCertDir(agentID) + + if !fileExists(certDir) { + return fmt.Errorf("agent certificate directory does not exist") + } + + if err := os.RemoveAll(certDir); err != nil { + return fmt.Errorf("failed to delete agent certificate directory: %w", err) + } + + slog.Info("Deleted agent certificate", "agent_id", agentID, "path", certDir) + return nil +} + +func (s *Service) ListAgentCerts() ([]string, error) { + if !fileExists(s.AgentCertDir) { + return []string{}, nil + } + + entries, err := os.ReadDir(s.AgentCertDir) + if err != nil { + return nil, fmt.Errorf("failed to read agent cert directory: %w", err) + } + + var agentIDs []string + for _, entry := range entries { + if entry.IsDir() { + agentID := entry.Name() + if s.AgentCertExists(agentID) { + agentIDs = append(agentIDs, agentID) + } + } + } + + return agentIDs, nil +} From 40487e6ae468b1893578b2ff3ebc2f06a5b29459 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 12:52:31 +0800 Subject: [PATCH 21/29] Update the test scripts --- misc/create-agent-cert.sh | 2 ++ misc/delete-agent-cert.sh | 2 ++ misc/get-agent-cert.sh | 2 ++ misc/list-connected-agents.sh | 2 ++ misc/provision-agent-cert.sh | 2 -- 5 files changed, 8 insertions(+), 2 deletions(-) create mode 100755 misc/create-agent-cert.sh create mode 100755 misc/delete-agent-cert.sh create mode 100755 misc/get-agent-cert.sh create mode 100755 misc/list-connected-agents.sh delete mode 100755 misc/provision-agent-cert.sh diff --git a/misc/create-agent-cert.sh b/misc/create-agent-cert.sh new file mode 100755 index 0000000..b179900 --- /dev/null +++ b/misc/create-agent-cert.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X POST http://localhost:8080/agents/agent-1/certificate -o agent-1-certs.zip diff --git a/misc/delete-agent-cert.sh b/misc/delete-agent-cert.sh new file mode 100755 index 0000000..542acfc --- /dev/null +++ b/misc/delete-agent-cert.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X DELETE http://localhost:8080/agents/agent-1/certificate diff --git a/misc/get-agent-cert.sh b/misc/get-agent-cert.sh new file mode 100755 index 0000000..011178a --- /dev/null +++ b/misc/get-agent-cert.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X GET http://localhost:8080/agents/agent-1/certificate -o agent-1-certs.zip diff --git a/misc/list-connected-agents.sh b/misc/list-connected-agents.sh new file mode 100755 index 0000000..ce70643 --- /dev/null +++ b/misc/list-connected-agents.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -X GET http://localhost:8080/agents diff --git a/misc/provision-agent-cert.sh b/misc/provision-agent-cert.sh deleted file mode 100755 index 00e1a44..0000000 --- a/misc/provision-agent-cert.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -X POST http://localhost:8080/cert/agent -H "Content-Type: application/json" -d '{"agent_id": "agent-1"}' --output agent-1-certs.zip From 498f799bd691c4b3c4a55eee36318bd705d51698 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 14:30:47 +0800 Subject: [PATCH 22/29] Update temp server cert clear --- internal/api/http/router.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/api/http/router.go b/internal/api/http/router.go index a41791e..0092db9 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -33,9 +33,5 @@ func SetupRoute(engine *gin.Engine, srvs *Services) { agents.DELETE("/:id/certificate", certHandler.DeleteAgentCertificate) } - cert := engine.Group("/cert") - { - cert.GET("/agents", certHandler.ListAgents) - cert.DELETE("/server", certHandler.DeleteServerCerts) - } + engine.DELETE("/server", certHandler.DeleteServerCerts) } From 6334b13c67e083764be89348b1283866ca06b775 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 28 Jan 2026 14:34:51 +0800 Subject: [PATCH 23/29] Update temp endpoint --- internal/api/http/router.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/api/http/router.go b/internal/api/http/router.go index 0092db9..57e0eb4 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -33,5 +33,6 @@ func SetupRoute(engine *gin.Engine, srvs *Services) { agents.DELETE("/:id/certificate", certHandler.DeleteAgentCertificate) } - engine.DELETE("/server", certHandler.DeleteServerCerts) + // Temp endpoint for cleaning + engine.DELETE("/server-certs", certHandler.DeleteServerCerts) } From f9b4f1a289d06cc09542e7812d04fbff356be0a1 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 4 Feb 2026 21:41:28 +0800 Subject: [PATCH 24/29] Fix findings --- internal/api/http/dto/cert.go | 20 ----- internal/api/http/handler/cert.go | 142 +++++++----------------------- internal/cert/utils.go | 16 ++++ misc/delete-server-certs.sh | 2 +- 4 files changed, 48 insertions(+), 132 deletions(-) delete mode 100644 internal/api/http/dto/cert.go diff --git a/internal/api/http/dto/cert.go b/internal/api/http/dto/cert.go deleted file mode 100644 index 4446c91..0000000 --- a/internal/api/http/dto/cert.go +++ /dev/null @@ -1,20 +0,0 @@ -package dto - -import "time" - -type AgentCertInfo struct { - AgentID string `json:"agent_id"` - CreatedAt time.Time `json:"created_at"` - ExpiresAt time.Time `json:"expires_at"` - CertPath string `json:"cert_path"` -} - -type ListAgentsResponse struct { - Agents []AgentCertInfo `json:"agents"` - Count int `json:"count"` -} - -type DeleteCertificateResponse struct { - Message string `json:"message"` - DeletedPaths []string `json:"deleted_paths,omitempty"` -} diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 807a10e..445404e 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -5,17 +5,26 @@ import ( "bytes" "crypto/rsa" "crypto/x509" - "encoding/pem" "fmt" "log/slog" "net/http" "os" - "github.com/EternisAI/silo-proxy/internal/api/http/dto" "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 { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + return "", false + } + return agentID, true +} + type CertHandler struct { certService *cert.Service } @@ -35,11 +44,8 @@ func (h *CertHandler) CreateAgentCertificate(ctx *gin.Context) { return } - agentID := ctx.Param("id") - if agentID == "" { - ctx.JSON(http.StatusBadRequest, gin.H{ - "error": "agent_id is required", - }) + agentID, ok := h.validateAgentID(ctx) + if !ok { return } @@ -95,11 +101,8 @@ func (h *CertHandler) GetAgentCertificate(ctx *gin.Context) { return } - agentID := ctx.Param("id") - if agentID == "" { - ctx.JSON(http.StatusBadRequest, gin.H{ - "error": "agent_id is required", - }) + agentID, ok := h.validateAgentID(ctx) + if !ok { return } @@ -130,37 +133,11 @@ func (h *CertHandler) GetAgentCertificate(ctx *gin.Context) { return } - zipBuffer := new(bytes.Buffer) - zipWriter := zip.NewWriter(zipBuffer) - - files := map[string][]byte{ - fmt.Sprintf("%s-cert.pem", agentID): agentCertBytes, - fmt.Sprintf("%s-key.pem", agentID): agentKeyBytes, - "ca-cert.pem": caCertBytes, - } - - for filename, content := range files { - f, err := zipWriter.Create(filename) - if err != nil { - slog.Error("Failed to create zip file entry", "error", err, "filename", filename) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to create zip file", - }) - return - } - if _, err := f.Write(content); err != nil { - slog.Error("Failed to write to zip file", "error", err, "filename", filename) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to write zip file", - }) - return - } - } - - if err := zipWriter.Close(); err != nil { - slog.Error("Failed to close zip writer", "error", err) + 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 finalize zip file", + "error": "Failed to create zip file", }) return } @@ -172,62 +149,6 @@ func (h *CertHandler) GetAgentCertificate(ctx *gin.Context) { slog.Info("Agent certificate retrieved successfully", "agent_id", agentID, "zip_size", zipBuffer.Len()) } -func (h *CertHandler) ListAgents(ctx *gin.Context) { - if h.certService == nil { - slog.Warn("Agent list requested but TLS is disabled") - ctx.JSON(http.StatusBadRequest, gin.H{ - "error": "TLS is not enabled on this server", - }) - return - } - - agentIDs, err := h.certService.ListAgentCerts() - if err != nil { - slog.Error("Failed to list agent certificates", "error", err) - ctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to list agent certificates", - }) - return - } - - agents := make([]dto.AgentCertInfo, 0, len(agentIDs)) - for _, agentID := range agentIDs { - certPath := h.certService.GetAgentCertPath(agentID) - agentCertBytes, _, err := h.certService.GetAgentCert(agentID) - if err != nil { - slog.Warn("Failed to read agent certificate for listing", "error", err, "agent_id", agentID) - continue - } - - block, _ := pem.Decode(agentCertBytes) - if block == nil { - slog.Warn("Failed to decode PEM certificate", "agent_id", agentID) - continue - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - slog.Warn("Failed to parse certificate", "error", err, "agent_id", agentID) - continue - } - - agents = append(agents, dto.AgentCertInfo{ - AgentID: agentID, - CreatedAt: cert.NotBefore, - ExpiresAt: cert.NotAfter, - CertPath: certPath, - }) - } - - response := dto.ListAgentsResponse{ - Agents: agents, - Count: len(agents), - } - - ctx.JSON(http.StatusOK, response) - slog.Info("Listed agent certificates", "count", len(agents)) -} - func (h *CertHandler) DeleteAgentCertificate(ctx *gin.Context) { if h.certService == nil { slog.Warn("Agent cert deletion requested but TLS is disabled") @@ -237,11 +158,8 @@ func (h *CertHandler) DeleteAgentCertificate(ctx *gin.Context) { return } - agentID := ctx.Param("id") - if agentID == "" { - ctx.JSON(http.StatusBadRequest, gin.H{ - "error": "agent_id is required", - }) + agentID, ok := h.validateAgentID(ctx) + if !ok { return } @@ -263,12 +181,10 @@ func (h *CertHandler) DeleteAgentCertificate(ctx *gin.Context) { return } - response := dto.DeleteCertificateResponse{ - Message: "Successfully deleted agent certificate", - DeletedPaths: []string{certDir}, - } - - ctx.JSON(http.StatusOK, response) + ctx.JSON(http.StatusOK, gin.H{ + "message": "Successfully deleted agent certificate", + "deleted_paths": []string{certDir}, + }) slog.Info("Agent certificate deleted successfully", "agent_id", agentID) } @@ -283,12 +199,16 @@ func (h *CertHandler) createCertZip(agentID string, agentCert *x509.Certificate, 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) files := map[string][]byte{ - fmt.Sprintf("%s-cert.pem", agentID): agentCertPEM, - fmt.Sprintf("%s-key.pem", agentID): agentKeyPEM, + fmt.Sprintf("%s-cert.pem", agentID): certPEM, + fmt.Sprintf("%s-key.pem", agentID): keyPEM, "ca-cert.pem": caCertBytes, } diff --git a/internal/cert/utils.go b/internal/cert/utils.go index 957b17d..548c497 100644 --- a/internal/cert/utils.go +++ b/internal/cert/utils.go @@ -7,9 +7,25 @@ import ( "encoding/pem" "fmt" "os" + "regexp" "strings" ) +var validAgentIDRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]*$`) + +func ValidateAgentID(agentID string) error { + if agentID == "" { + return fmt.Errorf("agent ID cannot be empty") + } + if len(agentID) > 64 { + return fmt.Errorf("agent ID too long (max 64 characters)") + } + if !validAgentIDRegex.MatchString(agentID) { + return fmt.Errorf("agent ID contains invalid characters (allowed: alphanumeric, underscore, hyphen)") + } + return nil +} + func ParseCommaSeparated(input string) []string { if input == "" { return nil diff --git a/misc/delete-server-certs.sh b/misc/delete-server-certs.sh index 0d359b0..20b0fb3 100755 --- a/misc/delete-server-certs.sh +++ b/misc/delete-server-certs.sh @@ -1,2 +1,2 @@ #!/bin/bash -curl -X DELETE http://localhost:8080/cert/server +curl -X DELETE http://localhost:8080/server-certs From a91121aec3afadf4be4929447f4bf8012db77464 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 4 Feb 2026 21:46:03 +0800 Subject: [PATCH 25/29] cleanup delete endpoint and add admin api key --- cmd/silo-proxy-agent/main.go | 2 +- cmd/silo-proxy-server/application.yml | 1 + cmd/silo-proxy-server/main.go | 4 +- internal/api/http/handler/cert.go | 54 --------------------------- internal/api/http/http.go | 1 + internal/api/http/middleware/auth.go | 47 +++++++++++++++++++++++ internal/api/http/router.go | 18 ++++----- 7 files changed, 61 insertions(+), 66 deletions(-) create mode 100644 internal/api/http/middleware/auth.go diff --git a/cmd/silo-proxy-agent/main.go b/cmd/silo-proxy-agent/main.go index 2d28bdb..10e2ce8 100644 --- a/cmd/silo-proxy-agent/main.go +++ b/cmd/silo-proxy-agent/main.go @@ -51,7 +51,7 @@ func main() { MaxAge: 12 * time.Hour, })) engine.Use(gin.Recovery()) - internalhttp.SetupRoute(engine, services) + internalhttp.SetupRoute(engine, services, "") server := &http.Server{ Addr: fmt.Sprintf(":%d", config.Http.Port), diff --git a/cmd/silo-proxy-server/application.yml b/cmd/silo-proxy-server/application.yml index e1c049b..8957817 100644 --- a/cmd/silo-proxy-server/application.yml +++ b/cmd/silo-proxy-server/application.yml @@ -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 diff --git a/cmd/silo-proxy-server/main.go b/cmd/silo-proxy-server/main.go index b488cfc..26b9788 100644 --- a/cmd/silo-proxy-server/main.go +++ b/cmd/silo-proxy-server/main.go @@ -81,13 +81,13 @@ func main() { 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), diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 445404e..773ef0c 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -8,7 +8,6 @@ import ( "fmt" "log/slog" "net/http" - "os" "github.com/EternisAI/silo-proxy/internal/cert" "github.com/gin-gonic/gin" @@ -228,56 +227,3 @@ func (h *CertHandler) createCertZipFromBytes(agentID string, certPEM, keyPEM, ca return zipBuffer, nil } - -func (h *CertHandler) DeleteServerCerts(ctx *gin.Context) { - if h.certService == nil { - slog.Warn("Server cert deletion requested but TLS is disabled") - ctx.JSON(http.StatusBadRequest, gin.H{ - "error": "TLS is not enabled on this server", - }) - return - } - - slog.Info("Deleting all server certificates") - - certPaths := []string{ - h.certService.CaCertPath, - h.certService.CaKeyPath, - h.certService.ServerCertPath, - h.certService.ServerKeyPath, - } - - deletedFiles := []string{} - var errors []string - - for _, path := range certPaths { - if _, err := os.Stat(path); os.IsNotExist(err) { - slog.Debug("Certificate file does not exist, skipping", "path", path) - continue - } - - if err := os.Remove(path); err != nil { - slog.Error("Failed to delete certificate file", "error", err, "path", path) - errors = append(errors, fmt.Sprintf("%s: %v", path, err)) - } else { - slog.Info("Deleted certificate file", "path", path) - deletedFiles = append(deletedFiles, path) - } - } - - if len(errors) > 0 { - ctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to delete some certificate files", - "deleted_files": deletedFiles, - "errors": errors, - }) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "message": "Successfully deleted server certificates", - "deleted_files": deletedFiles, - }) - - slog.Info("Server certificates deleted successfully", "count", len(deletedFiles)) -} diff --git a/internal/api/http/http.go b/internal/api/http/http.go index 8f65cac..6d4c874 100644 --- a/internal/api/http/http.go +++ b/internal/api/http/http.go @@ -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 { diff --git a/internal/api/http/middleware/auth.go b/internal/api/http/middleware/auth.go new file mode 100644 index 0000000..b1cffc2 --- /dev/null +++ b/internal/api/http/middleware/auth.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "crypto/subtle" + "log/slog" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + apiKeyHeader = "X-API-Key" +) + +func APIKeyAuth(apiKey string) gin.HandlerFunc { + return func(c *gin.Context) { + if apiKey == "" { + slog.Warn("Admin API key not configured, rejecting request", + "path", c.Request.URL.Path, + "client_ip", c.ClientIP()) + c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ + "error": "Admin API is not configured", + }) + return + } + + providedKey := c.GetHeader(apiKeyHeader) + if providedKey == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "error": "Missing API key", + }) + return + } + + if subtle.ConstantTimeCompare([]byte(providedKey), []byte(apiKey)) != 1 { + slog.Warn("Invalid API key attempt", + "path", c.Request.URL.Path, + "client_ip", c.ClientIP()) + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "error": "Invalid API key", + }) + return + } + + c.Next() + } +} diff --git a/internal/api/http/router.go b/internal/api/http/router.go index 57e0eb4..87a9e9c 100644 --- a/internal/api/http/router.go +++ b/internal/api/http/router.go @@ -13,14 +13,12 @@ type Services struct { CertService *cert.Service } -func SetupRoute(engine *gin.Engine, srvs *Services) { +func SetupRoute(engine *gin.Engine, srvs *Services, adminAPIKey string) { engine.Use(middleware.RequestLogger()) healthHandler := handler.NewHealthHandler() engine.GET("/health", healthHandler.Check) - certHandler := handler.NewCertHandler(srvs.CertService) - agents := engine.Group("/agents") { if srvs.GrpcServer != nil { @@ -28,11 +26,13 @@ func SetupRoute(engine *gin.Engine, srvs *Services) { agents.GET("", adminHandler.ListAgents) } - agents.POST("/:id/certificate", certHandler.CreateAgentCertificate) - agents.GET("/:id/certificate", certHandler.GetAgentCertificate) - agents.DELETE("/:id/certificate", certHandler.DeleteAgentCertificate) + certHandler := handler.NewCertHandler(srvs.CertService) + certRoutes := agents.Group("") + certRoutes.Use(middleware.APIKeyAuth(adminAPIKey)) + { + certRoutes.POST("/:id/certificate", certHandler.CreateAgentCertificate) + certRoutes.GET("/:id/certificate", certHandler.GetAgentCertificate) + certRoutes.DELETE("/:id/certificate", certHandler.DeleteAgentCertificate) + } } - - // Temp endpoint for cleaning - engine.DELETE("/server-certs", certHandler.DeleteServerCerts) } From dc5b0ee1532091caaa2136b315bcacca65da1c87 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 4 Feb 2026 21:46:10 +0800 Subject: [PATCH 26/29] Update test scripts --- misc/create-agent-cert.sh | 6 +++++- misc/delete-agent-cert.sh | 5 ++++- misc/delete-server-certs.sh | 2 -- misc/get-agent-cert.sh | 6 +++++- 4 files changed, 14 insertions(+), 5 deletions(-) delete mode 100755 misc/delete-server-certs.sh diff --git a/misc/create-agent-cert.sh b/misc/create-agent-cert.sh index b179900..0e66a07 100755 --- a/misc/create-agent-cert.sh +++ b/misc/create-agent-cert.sh @@ -1,2 +1,6 @@ #!/bin/bash -curl -X POST http://localhost:8080/agents/agent-1/certificate -o agent-1-certs.zip +AGENT_ID=${1:-agent-1} +API_KEY=${ADMIN_API_KEY:-your-api-key-here} +curl -X POST "http://localhost:8080/agents/${AGENT_ID}/certificate" \ + -H "X-API-Key: ${API_KEY}" \ + -o "${AGENT_ID}-certs.zip" diff --git a/misc/delete-agent-cert.sh b/misc/delete-agent-cert.sh index 542acfc..76c4a1e 100755 --- a/misc/delete-agent-cert.sh +++ b/misc/delete-agent-cert.sh @@ -1,2 +1,5 @@ #!/bin/bash -curl -X DELETE http://localhost:8080/agents/agent-1/certificate +AGENT_ID=${1:-agent-1} +API_KEY=${ADMIN_API_KEY:-your-api-key-here} +curl -X DELETE "http://localhost:8080/agents/${AGENT_ID}/certificate" \ + -H "X-API-Key: ${API_KEY}" diff --git a/misc/delete-server-certs.sh b/misc/delete-server-certs.sh deleted file mode 100755 index 20b0fb3..0000000 --- a/misc/delete-server-certs.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -X DELETE http://localhost:8080/server-certs diff --git a/misc/get-agent-cert.sh b/misc/get-agent-cert.sh index 011178a..c00fb22 100755 --- a/misc/get-agent-cert.sh +++ b/misc/get-agent-cert.sh @@ -1,2 +1,6 @@ #!/bin/bash -curl -X GET http://localhost:8080/agents/agent-1/certificate -o agent-1-certs.zip +AGENT_ID=${1:-agent-1} +API_KEY=${ADMIN_API_KEY:-your-api-key-here} +curl -X GET "http://localhost:8080/agents/${AGENT_ID}/certificate" \ + -H "X-API-Key: ${API_KEY}" \ + -o "${AGENT_ID}-certs.zip" From 60a7854c0777e90f342bdc522ed5ca88de33d366 Mon Sep 17 00:00:00 2001 From: jckhoe Date: Wed, 4 Feb 2026 21:47:58 +0800 Subject: [PATCH 27/29] Update error logging --- internal/api/http/handler/cert.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 773ef0c..c8a6f8d 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -16,6 +16,7 @@ import ( 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(), }) @@ -49,6 +50,7 @@ func (h *CertHandler) CreateAgentCertificate(ctx *gin.Context) { } 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", }) @@ -106,6 +108,7 @@ func (h *CertHandler) GetAgentCertificate(ctx *gin.Context) { } 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", }) @@ -163,6 +166,7 @@ func (h *CertHandler) DeleteAgentCertificate(ctx *gin.Context) { } 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", }) From 3c9eab9a951a47f632b32e132b06e4eef07a5cf0 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 5 Feb 2026 20:34:59 +0800 Subject: [PATCH 28/29] use defer.Close() --- docs/potential_issues.md | 5 +++++ internal/api/http/handler/cert.go | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 docs/potential_issues.md diff --git a/docs/potential_issues.md b/docs/potential_issues.md new file mode 100644 index 0000000..eba7f70 --- /dev/null +++ b/docs/potential_issues.md @@ -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 diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index c8a6f8d..9ce2e99 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -208,6 +208,7 @@ func (h *CertHandler) createCertZip(agentID string, agentCert *x509.Certificate, 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, @@ -225,9 +226,5 @@ func (h *CertHandler) createCertZipFromBytes(agentID string, certPEM, keyPEM, ca } } - if err := zipWriter.Close(); err != nil { - return nil, fmt.Errorf("failed to close zip writer: %w", err) - } - return zipBuffer, nil } From d6c3a0bf9a6dc2ad069dafce04f71d557dd30f57 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 5 Feb 2026 20:40:56 +0800 Subject: [PATCH 29/29] revert --- internal/api/http/handler/cert.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/api/http/handler/cert.go b/internal/api/http/handler/cert.go index 9ce2e99..13754cc 100644 --- a/internal/api/http/handler/cert.go +++ b/internal/api/http/handler/cert.go @@ -226,5 +226,9 @@ func (h *CertHandler) createCertZipFromBytes(agentID string, certPEM, keyPEM, ca } } + if err := zipWriter.Close(); err != nil { + return nil, fmt.Errorf("failed to finalize zip archive: %w", err) + } + return zipBuffer, nil }