mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 21:53:48 +03:00
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:
parent
fef9cb3e05
commit
bfaf2a8201
8 changed files with 268 additions and 8 deletions
|
@ -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"
|
||||||
|
@ -28,9 +29,10 @@ func init() {
|
||||||
//
|
//
|
||||||
// pki {
|
// pki {
|
||||||
// ca [<id>] {
|
// ca [<id>] {
|
||||||
// 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)
|
||||||
|
|
108
caddytest/integration/caddyfile_adapt/acme_server_lifetime.txt
Normal file
108
caddytest/integration/caddyfile_adapt/acme_server_lifetime.txt
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -165,4 +165,4 @@ acme-bar.example.com {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
101
caddytest/integration/pki_test.go
Normal file
101
caddytest/integration/pki_test.go
Normal 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)")
|
||||||
|
}
|
|
@ -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)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue