clavitor/clavis/clavis-vault/lib/tls.go

90 lines
2.4 KiB
Go

package lib
import (
"context"
"fmt"
"log"
"net/http"
"os"
"github.com/caddyserver/certmagic"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez/v3/acme"
)
// TLSConfig holds configuration for automatic TLS via Let's Encrypt DNS-01.
type TLSConfig struct {
Domain string // e.g. "use1.clavitor.ai"
CFToken string // Cloudflare API token for DNS-01 challenge
DataDir string // directory to store certs
Email string // ACME account email
}
// LoadTLSConfig reads TLS configuration from environment variables.
func LoadTLSConfig() TLSConfig {
return TLSConfig{
Domain: os.Getenv("TLS_DOMAIN"),
CFToken: os.Getenv("CF_API_TOKEN"),
DataDir: os.Getenv("TLS_CERT_DIR"),
Email: os.Getenv("TLS_EMAIL"),
}
}
// ListenAndServeTLS starts an HTTPS server on the given address using
// Let's Encrypt certificates obtained via Cloudflare DNS-01 challenge.
// Falls back to plain HTTP if TLS is not configured.
func ListenAndServeTLS(addr string, handler http.Handler, cfg TLSConfig) error {
if cfg.Domain == "" || cfg.CFToken == "" {
log.Printf("TLS not configured (missing TLS_DOMAIN or CF_API_TOKEN), serving plain HTTP")
return http.ListenAndServe(addr, handler)
}
if cfg.DataDir == "" {
cfg.DataDir = "/opt/clavitor/certs"
}
if cfg.Email == "" {
cfg.Email = "ops@clavitor.ai"
}
// Configure certmagic
certmagic.DefaultACME.Agreed = true
certmagic.DefaultACME.Email = cfg.Email
certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{
DNSManager: certmagic.DNSManager{
DNSProvider: &cloudflare.Provider{
APIToken: cfg.CFToken,
},
},
}
magic := certmagic.NewDefault()
magic.Storage = &certmagic.FileStorage{Path: cfg.DataDir}
// Obtain/renew cert
ctx := context.Background()
if err := magic.ManageSync(ctx, []string{cfg.Domain}); err != nil {
return fmt.Errorf("certmagic manage %s: %w", cfg.Domain, err)
}
tlsConfig := magic.TLSConfig()
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
server := &http.Server{
Addr: addr,
Handler: handler,
TLSConfig: tlsConfig,
}
log.Printf("Clavitor listening on https://0.0.0.0%s (%s)", addr, cfg.Domain)
// TLS certs are managed by certmagic, pass empty cert/key paths
return server.ListenAndServeTLS("", "")
}
// ChallengeError wraps ACME errors with actionable context.
func ChallengeError(err error) string {
if acmeErr, ok := err.(acme.Problem); ok {
return fmt.Sprintf("ACME error: %s (type: %s)", acmeErr.Detail, acmeErr.Type)
}
return err.Error()
}