mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-19 15:54:14 +01:00
350 lines
9.7 KiB
Markdown
350 lines
9.7 KiB
Markdown
# 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 provider
|
|
- `internal/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`)
|
|
|
|
```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`)
|
|
|
|
```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`)
|
|
|
|
```go
|
|
type User struct {
|
|
Email string // Account email
|
|
Registration *registration.Resource // ACME registration
|
|
Key crypto.PrivateKey // Account key
|
|
}
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### Certificate Lifecycle
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```yaml
|
|
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
|
|
|
|
```yaml
|
|
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 implementation
|
|
- `github.com/rs/zerolog` - Structured logging
|
|
|
|
### Internal Dependencies
|
|
|
|
- `internal/task/task.go` - Lifetime management
|
|
- `internal/notif/` - Renewal notifications
|
|
- `internal/config/` - Configuration loading
|
|
- `internal/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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// WebSocket endpoint: GET /api/v1/cert/renew
|
|
if provider.ForceExpiryAll() {
|
|
// Wait for renewal to complete
|
|
provider.WaitRenewalDone(ctx)
|
|
}
|
|
```
|
|
|
|
## Testing Notes
|
|
|
|
- `config_test.go` - Configuration validation
|
|
- `provider_test/` - Provider functionality tests
|
|
- `sni_test.go` - SNI matching tests
|
|
- `multi_cert_test.go` - Extra provider tests
|
|
- Integration tests require mock DNS provider
|