main.go raw

   1  // orly-certs is a certificate management service that obtains and renews
   2  // wildcard SSL certificates from Let's Encrypt using DNS-01 challenges.
   3  //
   4  // It supports multiple DNS providers via the lego library and stores
   5  // certificates at a conventional file path for web apps to consume.
   6  //
   7  // Configuration is via environment variables:
   8  //   - ORLY_CERTS_DOMAIN: Wildcard domain (e.g., "*.myapp.com")
   9  //   - ORLY_CERTS_EMAIL: Email for Let's Encrypt account
  10  //   - ORLY_CERTS_DNS_PROVIDER: DNS provider name (cloudflare, route53, etc.)
  11  //   - ORLY_CERTS_OUTPUT_DIR: Certificate output directory (default: /var/cache/orly-certs)
  12  //
  13  // Provider-specific credentials are set via standard lego environment variables.
  14  // See https://go-acme.github.io/lego/dns/ for documentation.
  15  package main
  16  
  17  import (
  18  	"context"
  19  	"fmt"
  20  	"os"
  21  	"os/signal"
  22  	"syscall"
  23  	"time"
  24  
  25  	"next.orly.dev/pkg/lol"
  26  	"next.orly.dev/pkg/lol/chk"
  27  	"next.orly.dev/pkg/lol/log"
  28  )
  29  
  30  func main() {
  31  	cfg := loadConfig()
  32  	lol.SetLogLevel(cfg.LogLevel)
  33  
  34  	log.I.F("orly-certs starting")
  35  	log.I.F("  domain: %s", cfg.Domain)
  36  	log.I.F("  email: %s", cfg.Email)
  37  	log.I.F("  dns provider: %s", cfg.DNSProvider)
  38  	log.I.F("  output dir: %s", cfg.OutputDir)
  39  	log.I.F("  acme server: %s", cfg.ACMEServerURL())
  40  
  41  	ctx, cancel := context.WithCancel(context.Background())
  42  	defer cancel()
  43  
  44  	// Set up signal handling
  45  	sigs := make(chan os.Signal, 1)
  46  	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
  47  
  48  	go func() {
  49  		<-sigs
  50  		log.I.F("shutdown signal received")
  51  		cancel()
  52  	}()
  53  
  54  	// Create certificate manager
  55  	manager, err := NewCertManager(cfg)
  56  	if chk.E(err) {
  57  		log.F.F("failed to create certificate manager: %v", err)
  58  	}
  59  
  60  	// Initial certificate check/obtain
  61  	if err := manager.EnsureCertificate(); chk.E(err) {
  62  		log.F.F("failed to ensure certificate: %v", err)
  63  	}
  64  
  65  	// Start renewal loop
  66  	log.I.F("starting renewal check loop (interval: %s)", cfg.CheckInterval)
  67  	ticker := time.NewTicker(cfg.CheckInterval)
  68  	defer ticker.Stop()
  69  
  70  	for {
  71  		select {
  72  		case <-ticker.C:
  73  			if err := manager.CheckRenewal(); chk.E(err) {
  74  				log.E.F("renewal check failed: %v", err)
  75  			}
  76  		case <-ctx.Done():
  77  			log.I.F("orly-certs shutting down")
  78  			return
  79  		}
  80  	}
  81  }
  82  
  83  func usage() {
  84  	fmt.Fprintf(os.Stderr, `orly-certs - DNS-01 wildcard certificate manager
  85  
  86  Usage: orly-certs [options]
  87  
  88  Environment Variables:
  89    ORLY_CERTS_DOMAIN         Wildcard domain (e.g., *.myapp.com) [required]
  90    ORLY_CERTS_EMAIL          Email for Let's Encrypt account [required]
  91    ORLY_CERTS_DNS_PROVIDER   DNS provider name [required]
  92    ORLY_CERTS_OUTPUT_DIR     Certificate output directory [default: /var/cache/orly-certs]
  93    ORLY_CERTS_RENEW_DAYS     Renew when expiring within N days [default: 30]
  94    ORLY_CERTS_CHECK_INTERVAL Renewal check interval [default: 12h]
  95    ORLY_CERTS_ACME_SERVER    ACME server URL [default: production Let's Encrypt]
  96    ORLY_CERTS_LOG_LEVEL      Log level [default: info]
  97  
  98  Supported DNS Providers:
  99    cloudflare, route53, hetzner, digitalocean, google, namecheap, godaddy,
 100    ovh, vultr, linode, gandi, dnsimple, duckdns, azure, alidns, and 80+ more.
 101  
 102  Provider credentials are set via standard lego environment variables.
 103  See https://go-acme.github.io/lego/dns/ for documentation.
 104  
 105  Example:
 106    export CF_API_TOKEN="your-cloudflare-api-token"
 107    export ORLY_CERTS_DOMAIN="*.myapp.com"
 108    export ORLY_CERTS_EMAIL="admin@myapp.com"
 109    export ORLY_CERTS_DNS_PROVIDER="cloudflare"
 110    ./orly-certs
 111  `)
 112  }
 113