Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ To get certificates signed from LetsEncrypt use the following config options
| `dns01_txt.provider` | [Lego Provider](https://go-acme.github.io/lego/dns/index.html) CLI Flag name |
| `dns01_txt.dns_servers` | Use your authoritative DNS server's addresses to avoid caching/TTL problems |
| `dns01_txt.env_vars` | Environment variables specific to your Lego DNS Provider for authentication |
| `cert_obtain_timeout` | Seconds to wait for the CA to issue the certificate after finalization (default `30`). Raise it if your CA's post-finalization checks take longer — otherwise the order fails with `certificate: time limit exceeded` while the CA still issues the cert. CertiNext currently recommends `300` while a CT log operator delays their 30-day product. Must be below `request_timeout` |
| `request_timeout` | Seconds the whole certificate request may take end to end — registration, authorizations, finalization and the wait above (default `120`). Raise it together with `cert_obtain_timeout`, e.g. `330` for a `300` wait |


### Starting acme-proxy
Expand Down
34 changes: 33 additions & 1 deletion externalcas/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ type acmeProxyConfig struct {
// Certificate lifetime in days (optional)
CertLifetime int `json:"certlifetime,omitempty"`

// Seconds to wait for the external CA to issue the certificate after the order
// is finalized (optional, default 30 — lego's default). Some CAs run
// post-finalization checks that take longer than that; must stay below
// request_timeout so the outer context still bounds the whole request.
CertObtainTimeout int `json:"cert_obtain_timeout,omitempty"`

// Seconds the whole certificate request may take end to end — account
// registration, authorizations, finalization and the wait above (optional,
// default 120). Raise it together with cert_obtain_timeout.
RequestTimeoutSec int `json:"request_timeout,omitempty"`

// Lego provider connection variables for dns01 TXT challenge
Lego legoConfig `json:"dns01_txt"`

Expand Down Expand Up @@ -64,6 +75,15 @@ func (c *acmeProxyConfig) Validate() error {
if c.CertLifetime < 0 {
return errors.New("certlifetime cannot be negative")
}
if c.CertObtainTimeout < 0 {
return errors.New("cert_obtain_timeout cannot be negative")
}
if c.RequestTimeoutSec < 0 {
return errors.New("request_timeout cannot be negative")
}
if c.ObtainTimeout() >= c.RequestTimeout() {
return fmt.Errorf("cert_obtain_timeout (%s) must be less than request_timeout (%s)", c.ObtainTimeout(), c.RequestTimeout())
}

// Consider Metrics enabled only when port & datasource both are defined
if c.Metrics.Port > 0 && c.Metrics.DataSource != "" {
Expand All @@ -83,7 +103,19 @@ func (c *acmeProxyConfig) HTTPTimeout() time.Duration {

// RequestTimeout returns the timeout for certificate request operations
func (c *acmeProxyConfig) RequestTimeout() time.Duration {
return 2 * time.Minute
if c.RequestTimeoutSec <= 0 {
return 2 * time.Minute
}
return time.Duration(c.RequestTimeoutSec) * time.Second
}

// ObtainTimeout returns how long lego waits for the external CA to issue the
// certificate after finalization
func (c *acmeProxyConfig) ObtainTimeout() time.Duration {
if c.CertObtainTimeout <= 0 {
return 30 * time.Second
}
return time.Duration(c.CertObtainTimeout) * time.Second
}

// parseConfig is a helper function which reads ca.json file as rawjson and validates
Expand Down
83 changes: 83 additions & 0 deletions externalcas/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,72 @@ func TestAcmeProxyConfig_Validate(t *testing.T) {
},
wantErr: false,
},
{
name: "negative cert_obtain_timeout",
config: acmeProxyConfig{
CaURL: "https://acme.example.com",
Kid: "test-kid",
HmacKey: "test-hmac",
CertObtainTimeout: -1,
},
wantErr: true,
errMsg: "cert_obtain_timeout cannot be negative",
},
{
name: "cert_obtain_timeout at or above request timeout",
config: acmeProxyConfig{
CaURL: "https://acme.example.com",
Kid: "test-kid",
HmacKey: "test-hmac",
CertObtainTimeout: 120,
},
wantErr: true,
errMsg: "cert_obtain_timeout (2m0s) must be less than request_timeout (2m0s)",
},
{
name: "cert_obtain_timeout below request timeout is valid",
config: acmeProxyConfig{
CaURL: "https://acme.example.com",
Kid: "test-kid",
HmacKey: "test-hmac",
CertObtainTimeout: 90,
},
wantErr: false,
},
{
name: "negative request_timeout",
config: acmeProxyConfig{
CaURL: "https://acme.example.com",
Kid: "test-kid",
HmacKey: "test-hmac",
RequestTimeoutSec: -1,
},
wantErr: true,
errMsg: "request_timeout cannot be negative",
},
{
name: "cert_obtain_timeout above default request timeout is valid when request_timeout is raised",
config: acmeProxyConfig{
CaURL: "https://acme.example.com",
Kid: "test-kid",
HmacKey: "test-hmac",
CertObtainTimeout: 300,
RequestTimeoutSec: 330,
},
wantErr: false,
},
{
name: "cert_obtain_timeout at a raised request_timeout is rejected",
config: acmeProxyConfig{
CaURL: "https://acme.example.com",
Kid: "test-kid",
HmacKey: "test-hmac",
CertObtainTimeout: 330,
RequestTimeoutSec: 330,
},
wantErr: true,
errMsg: "cert_obtain_timeout (5m30s) must be less than request_timeout (5m30s)",
},
{
name: "metrics enabled without valid datasource",
config: acmeProxyConfig{
Expand Down Expand Up @@ -173,6 +239,23 @@ func TestAcmeProxyConfig_Timeouts(t *testing.T) {
if requestTimeout != 2*time.Minute {
t.Errorf("RequestTimeout() = %v, want %v", requestTimeout, 2*time.Minute)
}

obtainTimeout := config.ObtainTimeout()
if obtainTimeout != 30*time.Second {
t.Errorf("ObtainTimeout() default = %v, want %v", obtainTimeout, 30*time.Second)
}

config.CertObtainTimeout = 90
obtainTimeout = config.ObtainTimeout()
if obtainTimeout != 90*time.Second {
t.Errorf("ObtainTimeout() with cert_obtain_timeout=90 = %v, want %v", obtainTimeout, 90*time.Second)
}

config.RequestTimeoutSec = 330
requestTimeout = config.RequestTimeout()
if requestTimeout != 330*time.Second {
t.Errorf("RequestTimeout() with request_timeout=330 = %v, want %v", requestTimeout, 330*time.Second)
}
}

func TestParseConfig(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions externalcas/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func (c *ExternalCAS) createLegoClient(cfg *acmeProxyConfig) (ACMEClient, error)
clientConfig := lego.NewConfig(user)
clientConfig.CADirURL = cfg.CaURL
clientConfig.Certificate.KeyType = certcrypto.EC256 // gitleaks:allow
clientConfig.Certificate.Timeout = cfg.ObtainTimeout()
clientConfig.HTTPClient = &http.Client{
Timeout: cfg.HTTPTimeout(),
}
Expand Down