An ACME client designed to obtain publicly-trusted SSL/TLS certificates for internal resources.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: support storing certs from multiple providers

+57 -6
+51 -3
internal/storage/storage.go
··· 1 1 package storage 2 2 3 3 import ( 4 + "crypto/sha256" 4 5 "crypto/x509" 6 + "encoding/hex" 5 7 "encoding/pem" 6 8 "fmt" 7 9 "os" ··· 12 14 13 15 // Storage handles certificate and key storage 14 16 type Storage struct { 15 - basePath string 16 - certsDir string 17 - privateDir string 17 + basePath string 18 + certsDir string 19 + privateDir string 20 + directoryURL string 18 21 } 19 22 20 23 // New creates a new Storage instance ··· 24 27 certsDir: filepath.Join(basePath, certsDir), 25 28 privateDir: filepath.Join(basePath, privateDir), 26 29 } 30 + 31 + // Create directories 32 + if err := os.MkdirAll(s.certsDir, 0755); err != nil { 33 + return nil, fmt.Errorf("creating certs directory: %w", err) 34 + } 35 + if err := os.MkdirAll(s.privateDir, 0700); err != nil { 36 + return nil, fmt.Errorf("creating private directory: %w", err) 37 + } 38 + 39 + return s, nil 40 + } 41 + 42 + // NewWithDirectory creates a new Storage instance with a specific ACME directory URL 43 + // This allows certificates from different CAs (staging/production) to coexist 44 + func NewWithDirectory(basePath, certsDir, privateDir, directoryURL string) (*Storage, error) { 45 + s := &Storage{ 46 + basePath: basePath, 47 + directoryURL: directoryURL, 48 + } 49 + 50 + // Create subdirectory based on directory URL hash 51 + dirHash := hashDirectoryURL(directoryURL) 52 + s.certsDir = filepath.Join(basePath, certsDir, dirHash) 53 + s.privateDir = filepath.Join(basePath, privateDir, dirHash) 27 54 28 55 // Create directories 29 56 if err := os.MkdirAll(s.certsDir, 0755); err != nil { ··· 130 157 131 158 renewalTime := time.Now().Add(time.Duration(daysBeforeExpiry) * 24 * time.Hour) 132 159 return expiry.Before(renewalTime), nil 160 + } 161 + 162 + // DeleteCertificate deletes a certificate from disk 163 + func (s *Storage) DeleteCertificate(domains []string) error { 164 + name := sanitizeDomainName(domains[0]) 165 + certPath := filepath.Join(s.certsDir, name+".crt") 166 + return os.Remove(certPath) 167 + } 168 + 169 + // DeletePrivateKey deletes a private key from disk 170 + func (s *Storage) DeletePrivateKey(domains []string) error { 171 + name := sanitizeDomainName(domains[0]) 172 + keyPath := filepath.Join(s.privateDir, name+".key") 173 + return os.Remove(keyPath) 174 + } 175 + 176 + // hashDirectoryURL creates a short hash of the directory URL for file organization 177 + func hashDirectoryURL(url string) string { 178 + hash := sha256.Sum256([]byte(url)) 179 + // Use first 8 characters of hex for readability 180 + return hex.EncodeToString(hash[:])[:8] 133 181 } 134 182 135 183 // sanitizeDomainName converts a domain name to a safe filename
+6 -3
main.go
··· 54 54 } 55 55 log.Printf("Loaded configuration from: %s", *configPath) 56 56 57 - // Initialize storage 58 - stor, err := storage.New(cfg.Storage.BasePath, cfg.Storage.CertsDir, cfg.Storage.PrivateDir) 57 + // Get directory URL early so we can use it for storage 58 + directoryURL := cfg.ACME.GetDirectoryURL() 59 + 60 + // Initialize storage with directory URL to separate staging/production certs 61 + stor, err := storage.NewWithDirectory(cfg.Storage.BasePath, cfg.Storage.CertsDir, cfg.Storage.PrivateDir, directoryURL) 59 62 if err != nil { 60 63 log.Fatalf("Failed to initialize storage: %v", err) 61 64 } ··· 71 74 72 75 // Initialize ACME client 73 76 accountDir := filepath.Join(cfg.Storage.BasePath, cfg.Storage.AccountDir) 74 - acmeClient, err := acme.NewClient(cfg.ACME.GetDirectoryURL(), cfg.ACME.Email, accountDir, cfg.ACME.AcceptTOS) 77 + acmeClient, err := acme.NewClient(directoryURL, cfg.ACME.Email, accountDir, cfg.ACME.AcceptTOS) 75 78 if err != nil { 76 79 log.Fatalf("Failed to create ACME client: %v", err) 77 80 }