acme_server: Configurable default lifetime for issued certificates (#5232)

* acme_server: add certificate lifetime configuration option

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>

* pki: allow intermediate cert lifetime to be configured

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>
This commit is contained in:
Kyle McCullough 2022-12-05 23:12:26 -08:00 committed by GitHub
parent fef9cb3e05
commit bfaf2a8201
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 268 additions and 8 deletions

View file

@ -15,6 +15,7 @@
package httpcaddyfile package httpcaddyfile
import ( import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
@ -31,6 +32,7 @@ func init() {
// name <name> // name <name>
// root_cn <name> // root_cn <name>
// intermediate_cn <name> // intermediate_cn <name>
// intermediate_lifetime <duration>
// root { // root {
// cert <path> // cert <path>
// key <path> // key <path>
@ -83,6 +85,16 @@ func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
} }
pkiCa.IntermediateCommonName = d.Val() pkiCa.IntermediateCommonName = d.Val()
case "intermediate_lifetime":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
pkiCa.IntermediateLifetime = caddy.Duration(dur)
case "root": case "root":
if pkiCa.Root == nil { if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair) pkiCa.Root = new(caddypki.KeyPair)

View file

@ -0,0 +1,108 @@
{
pki {
ca internal {
name "Internal"
root_cn "Internal Root Cert"
intermediate_cn "Internal Intermediate Cert"
}
ca internal-long-lived {
name "Long-lived"
root_cn "Internal Root Cert 2"
intermediate_cn "Internal Intermediate Cert 2"
}
}
}
acme-internal.example.com {
acme_server {
ca internal
}
}
acme-long-lived.example.com {
acme_server {
ca internal-long-lived
lifetime 7d
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme-long-lived.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal-long-lived",
"handler": "acme_server",
"lifetime": 604800000000000
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme-internal.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"name": "Internal",
"root_common_name": "Internal Root Cert",
"intermediate_common_name": "Internal Intermediate Cert"
},
"internal-long-lived": {
"name": "Long-lived",
"root_common_name": "Internal Root Cert 2",
"intermediate_common_name": "Internal Intermediate Cert 2"
}
}
}
}
}

View file

@ -0,0 +1,101 @@
package integration
import (
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func TestLeafCertLifetimeLessThanIntermediate(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server",
"lifetime": 604800000000000
}
]
}
]
}
]
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"install_trust": false,
"intermediate_lifetime": 604800000000000,
"name": "Internal CA"
}
}
}
}
}
`, "json", "certificate lifetime (168h0m0s) should be less than intermediate certificate lifetime (168h0m0s)")
}
func TestIntermediateLifetimeLessThanRoot(t *testing.T) {
caddytest.AssertLoadError(t, `
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server",
"lifetime": 2592000000000000
}
]
}
]
}
]
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"install_trust": false,
"intermediate_lifetime": 311040000000000000,
"name": "Internal CA"
}
}
}
}
}
`, "json", "intermediate certificate lifetime must be less than root certificate lifetime (86400h0m0s)")
}

View file

@ -48,6 +48,9 @@ type Handler struct {
// the default ID is "local". // the default ID is "local".
CA string `json:"ca,omitempty"` CA string `json:"ca,omitempty"`
// The lifetime for issued certificates
Lifetime caddy.Duration `json:"lifetime,omitempty"`
// The hostname or IP address by which ACME clients // The hostname or IP address by which ACME clients
// will access the server. This is used to populate // will access the server. This is used to populate
// the ACME directory endpoint. If not set, the Host // the ACME directory endpoint. If not set, the Host
@ -95,6 +98,9 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
if ash.PathPrefix == "" { if ash.PathPrefix == "" {
ash.PathPrefix = defaultPathPrefix ash.PathPrefix = defaultPathPrefix
} }
if ash.Lifetime == 0 {
ash.Lifetime = caddy.Duration(12 * time.Hour)
}
// get a reference to the configured CA // get a reference to the configured CA
appModule, err := ctx.App("pki") appModule, err := ctx.App("pki")
@ -107,6 +113,12 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
return err return err
} }
// make sure leaf cert lifetime is less than the intermediate cert lifetime. this check only
// applies for caddy-managed intermediate certificates
if ca.Intermediate == nil && ash.Lifetime >= ca.IntermediateLifetime {
return fmt.Errorf("certificate lifetime (%s) should be less than intermediate certificate lifetime (%s)", time.Duration(ash.Lifetime), time.Duration(ca.IntermediateLifetime))
}
database, err := ash.openDatabase() database, err := ash.openDatabase()
if err != nil { if err != nil {
return err return err
@ -122,7 +134,7 @@ func (ash *Handler) Provision(ctx caddy.Context) error {
Claims: &provisioner.Claims{ Claims: &provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365}, MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365},
DefaultTLSDur: &provisioner.Duration{Duration: 12 * time.Hour}, DefaultTLSDur: &provisioner.Duration{Duration: time.Duration(ash.Lifetime)},
}, },
}, },
}, },

View file

@ -15,6 +15,9 @@
package acmeserver package acmeserver
import ( import (
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
) )
@ -27,6 +30,7 @@ func init() {
// //
// acme_server [<matcher>] { // acme_server [<matcher>] {
// ca <id> // ca <id>
// lifetime <duration>
// } // }
func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() { if !h.Next() {
@ -55,6 +59,21 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
ca = new(caddypki.CA) ca = new(caddypki.CA)
} }
ca.ID = acmeServer.CA ca.ID = acmeServer.CA
case "lifetime":
if !h.NextArg() {
return nil, h.ArgErr()
}
dur, err := caddy.ParseDuration(h.Val())
if err != nil {
return nil, err
}
if d := time.Duration(ca.IntermediateLifetime); d > 0 && dur > d {
return nil, h.Errf("certificate lifetime (%s) exceeds intermediate certificate lifetime (%s)", dur, d)
}
acmeServer.Lifetime = caddy.Duration(dur)
} }
} }
} }

View file

@ -48,6 +48,9 @@ type CA struct {
// intermediate certificates. // intermediate certificates.
IntermediateCommonName string `json:"intermediate_common_name,omitempty"` IntermediateCommonName string `json:"intermediate_common_name,omitempty"`
// The lifetime for the intermediate certificates
IntermediateLifetime caddy.Duration `json:"intermediate_lifetime,omitempty"`
// Whether Caddy will attempt to install the CA's root // Whether Caddy will attempt to install the CA's root
// into the system trust store, as well as into Java // into the system trust store, as well as into Java
// and Mozilla Firefox trust stores. Default: true. // and Mozilla Firefox trust stores. Default: true.
@ -118,6 +121,11 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
if ca.IntermediateCommonName == "" { if ca.IntermediateCommonName == "" {
ca.IntermediateCommonName = defaultIntermediateCommonName ca.IntermediateCommonName = defaultIntermediateCommonName
} }
if ca.IntermediateLifetime == 0 {
ca.IntermediateLifetime = caddy.Duration(defaultIntermediateLifetime)
} else if time.Duration(ca.IntermediateLifetime) >= defaultRootLifetime {
return fmt.Errorf("intermediate certificate lifetime must be less than root certificate lifetime (%s)", defaultRootLifetime)
}
// load the certs and key that will be used for signing // load the certs and key that will be used for signing
var rootCert, interCert *x509.Certificate var rootCert, interCert *x509.Certificate
@ -341,7 +349,7 @@ func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Si
func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) { func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCert *x509.Certificate, interKey crypto.Signer, err error) {
repl := ca.newReplacer() repl := ca.newReplacer()
interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey) interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey, time.Duration(ca.IntermediateLifetime))
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("generating CA intermediate: %v", err) return nil, nil, fmt.Errorf("generating CA intermediate: %v", err)
} }

View file

@ -35,8 +35,8 @@ func generateRoot(commonName string) (*x509.Certificate, crypto.Signer, error) {
return root, signer, nil return root, signer, nil
} }
func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey crypto.Signer) (*x509.Certificate, crypto.Signer, error) { func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey crypto.Signer, lifetime time.Duration) (*x509.Certificate, crypto.Signer, error) {
template, signer, err := newCert(commonName, x509util.DefaultIntermediateTemplate, defaultIntermediateLifetime) template, signer, err := newCert(commonName, x509util.DefaultIntermediateTemplate, lifetime)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }