mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-24 11:24:52 +01:00
Replace YAML-specific functions with generic ones accepting unmarshaler/marshaler function parameters. This enables future support for JSON and other formats while maintaining current YAML behavior. - UnmarshalValidateYAML -> UnmarshalValidate(unmarshalFunc) - UnmarshalValidateYAMLXSync -> UnmarshalValidateXSync(unmarshalFunc) - SaveJSON -> SaveFile(marshalFunc) - LoadJSONIfExist -> LoadFileIfExist(unmarshalFunc) - Add UnmarshalValidateReader for reader-based decoding Testing: all 12 staged test files updated to use new API
Autocert Package
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).
Overview
Purpose
This package provides complete SSL certificate lifecycle management:
- ACME account registration and management
- Certificate issuance via DNS-01 challenge
- Automatic renewal scheduling (1 month before expiry)
- SNI-based certificate selection for multi-domain setups
Primary Consumers
goutils/server- TLS handshake certificate providerinternal/api/v1/cert/- REST API for certificate management- Configuration loading via
internal/config/
Non-goals
- HTTP-01 challenge support
- Certificate transparency log monitoring
- OCSP stapling
- Private CA support (except via custom CADirURL)
Stability
Internal package with stable public APIs. ACME protocol compliance depends on lego library.
Public API
Config (config.go)
type Config struct {
Email string // ACME account email
Domains []string // Domains to certify
CertPath string // Output cert path
KeyPath string // Output key path
Extra []ConfigExtra // Additional cert configs
ACMEKeyPath string // ACME account private key
Provider string // DNS provider name
Options map[string]strutils.Redacted // Provider options
Resolvers []string // DNS resolvers
CADirURL string // Custom ACME CA directory
CACerts []string // Custom CA certificates
EABKid string // External Account Binding Key ID
EABHmac string // External Account Binding HMAC
}
// Merge extra config with main provider
func MergeExtraConfig(mainCfg *Config, extraCfg *ConfigExtra) ConfigExtra
Provider (provider.go)
type Provider struct {
logger zerolog.Logger
cfg *Config
user *User
legoCfg *lego.Config
client *lego.Client
lastFailure time.Time
legoCert *certificate.Resource
tlsCert *tls.Certificate
certExpiries CertExpiries
extraProviders []*Provider
sniMatcher sniMatcher
}
// Create new provider (initializes extras atomically)
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error)
// TLS certificate getter for SNI
func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
// Certificate info for API
func (p *Provider) GetCertInfos() ([]CertInfo, error)
// Provider name ("main" or "extra[N]")
func (p *Provider) GetName() string
// Obtain certificate if not exists
func (p *Provider) ObtainCertIfNotExistsAll() error
// Force immediate renewal
func (p *Provider) ForceExpiryAll() bool
// Schedule automatic renewal
func (p *Provider) ScheduleRenewalAll(parent task.Parent)
// Print expiry dates
func (p *Provider) PrintCertExpiriesAll()
User (user.go)
type User struct {
Email string // Account email
Registration *registration.Resource // ACME registration
Key crypto.PrivateKey // Account key
}
Architecture
Certificate Lifecycle
flowchart TD
A[Start] --> B[Load Existing Cert]
B --> C{Cert Exists?}
C -->|Yes| D[Load Cert from Disk]
C -->|No| E[Obtain New Cert]
D --> F{Valid & Not Expired?}
F -->|Yes| G[Schedule Renewal]
F -->|No| H{Renewal Time?}
H -->|Yes| I[Renew Certificate]
H -->|No| G
E --> J[Init ACME Client]
J --> K[Register Account]
K --> L[DNS-01 Challenge]
L --> M[Complete Challenge]
M --> N[Download Certificate]
N --> O[Save to Disk]
O --> G
G --> P[Wait Until Renewal Time]
P --> Q[Trigger Renewal]
Q --> I
I --> R[Renew via ACME]
R --> S{Same Domains?}
S -->|Yes| T[Bundle & Save]
S -->|No| U[Re-obtain Certificate]
U --> T
T --> V[Update SNI Matcher]
V --> G
style E fill:#22553F,color:#fff
style I fill:#8B8000,color:#fff
style N fill:#22553F,color:#fff
style U fill:#84261A,color:#fff
SNI Matching Flow
flowchart LR
Client["TLS Client"] -->|ClientHello SNI| Proxy["GoDoxy Proxy"]
Proxy -->|Certificate| Client
subgraph "SNI Matching Process"
direction TB
A[Extract SNI from ClientHello] --> B{Normalize SNI}
B --> C{Exact Match?}
C -->|Yes| D[Return cert]
C -->|No| E[Wildcard Suffix Tree]
E --> F{Match Found?}
F -->|Yes| D
F -->|No| G[Return default cert]
end
style C fill:#27632A,color:#fff
style E fill:#18597A,color:#fff
style F fill:#836C03,color:#fff
Suffix Tree Structure
Certificate: *.example.com, example.com, *.api.example.com
exact:
"example.com" -> Provider_A
root:
└── "com"
└── "example"
├── "*" -> Provider_A [wildcard at *.example.com]
└── "api"
└── "*" -> Provider_B [wildcard at *.api.example.com]
Configuration Surface
Provider Types
| Type | Description | Use Case |
|---|---|---|
local |
No ACME, use existing cert | Pre-existing certificates |
pseudo |
Mock provider for testing | Development |
| ACME providers | Let's Encrypt, ZeroSSL, etc. | Production |
Supported DNS Providers
| Provider | Name | Required Options |
|---|---|---|
| Cloudflare | cloudflare |
CF_API_TOKEN |
| Route 53 | route53 |
AWS credentials |
| DigitalOcean | digitalocean |
DO_API_TOKEN |
| GoDaddy | godaddy |
GD_API_KEY, GD_API_SECRET |
| OVH | ovh |
OVH_ENDPOINT, OVH_APP_KEY, etc. |
| CloudDNS | clouddns |
GCP credentials |
| AzureDNS | azuredns |
Azure credentials |
| DuckDNS | duckdns |
DUCKDNS_TOKEN |
Example Configuration
autocert:
provider: cloudflare
email: admin@example.com
domains:
- example.com
- "*.example.com"
options:
auth_token: ${CF_API_TOKEN}
resolvers:
- 1.1.1.1:53
Extra Providers
autocert:
provider: cloudflare
email: admin@example.com
domains:
- example.com
- "*.example.com"
cert_path: certs/example.com.crt
key_path: certs/example.com.key
options:
auth_token: ${CF_API_TOKEN}
extra:
- domains:
- api.example.com
- "*.api.example.com"
cert_path: certs/api.example.com.crt
key_path: certs/api.example.com.key
Dependency and Integration Map
External Dependencies
github.com/go-acme/lego/v4- ACME protocol implementationgithub.com/rs/zerolog- Structured logging
Internal Dependencies
internal/task/task.go- Lifetime managementinternal/notif/- Renewal notificationsinternal/config/- Configuration loadinginternal/dnsproviders/- DNS provider implementations
Observability
Logs
| Level | When |
|---|---|
Info |
Certificate obtained/renewed |
Info |
Registration reused |
Warn |
Renewal failure |
Error |
Certificate retrieval failure |
Notifications
- Certificate renewal success/failure
- Service startup with expiry dates
Security Considerations
- Account private key stored at
certs/acme.key(mode 0600) - Certificate private keys stored at configured paths (mode 0600)
- Certificate files world-readable (mode 0644)
- ACME account email used for Let's Encrypt ToS
- EAB credentials for zero-touch enrollment
Failure Modes and Recovery
| Failure Mode | Impact | Recovery |
|---|---|---|
| DNS-01 challenge timeout | Certificate issuance fails | Check DNS provider API |
| Rate limiting (too many certs) | 1-hour cooldown | Wait or use different account |
| DNS provider API error | Renewal fails | 1-hour cooldown, retry |
| Certificate domains mismatch | Must re-obtain | Force renewal via API |
| Account key corrupted | Must register new account | New key, may lose certs |
Failure Tracking
Last failure persisted per-certificate to prevent rate limiting:
File: <cert_dir>/.last_failure-<hash>
Where hash = SHA256(certPath|keyPath)[:6]
Usage Examples
Initial Setup
autocertCfg := state.AutoCert
user, legoCfg, err := autocertCfg.GetLegoConfig()
if err != nil {
return err
}
provider, err := autocert.NewProvider(autocertCfg, user, legoCfg)
if err != nil {
return fmt.Errorf("autocert error: %w", err)
}
if err := provider.ObtainCertIfNotExistsAll(); err != nil {
return fmt.Errorf("failed to obtain certificates: %w", err)
}
provider.ScheduleRenewalAll(state.Task())
provider.PrintCertExpiriesAll()
Force Renewal via API
// WebSocket endpoint: GET /api/v1/cert/renew
if provider.ForceExpiryAll() {
// Wait for renewal to complete
provider.WaitRenewalDone(ctx)
}
Testing Notes
config_test.go- Configuration validationprovider_test/- Provider functionality testssni_test.go- SNI matching testsmulti_cert_test.go- Extra provider tests- Integration tests require mock DNS provider