bc7ec4455f
Use the new verifyLearfCert to show the cert verifies with intermediates from both sources. This required using the RPC interface so that the leaf pem was constructed correctly. Add IndexedCARoots.Active since that is a common operation we see in a few places.
601 lines
20 KiB
Go
601 lines
20 KiB
Go
package structs
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"time"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
"github.com/hashicorp/consul/lib"
|
|
)
|
|
|
|
const (
|
|
DefaultLeafCertTTL = "72h"
|
|
DefaultIntermediateCertTTL = "8760h" // ~ 1 year = 365 * 24h
|
|
DefaultRootCertTTL = "87600h" // ~ 10 years = 365 * 24h * 10
|
|
)
|
|
|
|
// IndexedCARoots is the list of currently trusted CA Roots.
|
|
type IndexedCARoots struct {
|
|
// ActiveRootID is the ID of a root in Roots that is the active CA root.
|
|
// Other roots are still valid if they're in the Roots list but are in
|
|
// the process of being rotated out.
|
|
ActiveRootID string
|
|
|
|
// TrustDomain is the identification root for this Consul cluster. All
|
|
// certificates signed by the cluster's CA must have their identifying URI in
|
|
// this domain.
|
|
//
|
|
// This does not include the protocol (currently spiffe://) since we may
|
|
// implement other protocols in future with equivalent semantics. It should be
|
|
// compared against the "authority" section of a URI (i.e. host:port).
|
|
//
|
|
// We need to support migrating a cluster between trust domains to support
|
|
// Multi-DC migration in Enterprise. In this case the current trust domain is
|
|
// here but entries in Roots may also have ExternalTrustDomain set to a
|
|
// non-empty value implying they were previous roots that are still trusted
|
|
// but under a different trust domain.
|
|
//
|
|
// Note that we DON'T validate trust domain during AuthZ since it causes
|
|
// issues of loss of connectivity during migration between trust domains. The
|
|
// only time the additional validation adds value is where the cluster shares
|
|
// an external root (e.g. organization-wide root) with another distinct Consul
|
|
// cluster or PKI system. In this case, x509 Name Constraints can be added to
|
|
// enforce that Consul's CA can only validly sign or trust certs within the
|
|
// same trust-domain. Name constraints as enforced by TLS handshake also allow
|
|
// seamless rotation between trust domains thanks to cross-signing.
|
|
TrustDomain string
|
|
|
|
// Roots is a list of root CA certs to trust.
|
|
Roots []*CARoot
|
|
|
|
// QueryMeta contains the meta sent via a header. We ignore for JSON
|
|
// so this whole structure can be returned.
|
|
QueryMeta `json:"-"`
|
|
}
|
|
|
|
func (r IndexedCARoots) Active() *CARoot {
|
|
for _, root := range r.Roots {
|
|
if root.ID == r.ActiveRootID {
|
|
return root
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CARoot represents a root CA certificate that is trusted.
|
|
type CARoot struct {
|
|
// ID is a globally unique ID (UUID) representing this CA root.
|
|
ID string
|
|
|
|
// Name is a human-friendly name for this CA root. This value is
|
|
// opaque to Consul and is not used for anything internally.
|
|
Name string
|
|
|
|
// SerialNumber is the x509 serial number of the certificate.
|
|
SerialNumber uint64
|
|
|
|
// SigningKeyID is the ID of the public key that corresponds to the private
|
|
// key used to sign leaf certificates. Is is the HexString format of the
|
|
// raw AuthorityKeyID bytes.
|
|
SigningKeyID string
|
|
|
|
// ExternalTrustDomain is the trust domain this root was generated under. It
|
|
// is usually empty implying "the current cluster trust-domain". It is set
|
|
// only in the case that a cluster changes trust domain and then all old roots
|
|
// that are still trusted have the old trust domain set here.
|
|
//
|
|
// We currently DON'T validate these trust domains explicitly anywhere, see
|
|
// IndexedRoots.TrustDomain doc. We retain this information for debugging and
|
|
// future flexibility.
|
|
ExternalTrustDomain string
|
|
|
|
// Time validity bounds.
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
|
|
// RootCert is the PEM-encoded public certificate for the root CA. The
|
|
// certificate is the same for all federated clusters.
|
|
RootCert string
|
|
|
|
// IntermediateCerts is a list of PEM-encoded intermediate certs to
|
|
// attach to any leaf certs signed by this CA. The list may include a
|
|
// certificate cross-signed by an old root CA, any subordinate CAs below the
|
|
// root CA, and the intermediate CA used to sign leaf certificates in the
|
|
// local Datacenter.
|
|
//
|
|
// If the provider which created this root uses an intermediate to sign
|
|
// leaf certificates (Vault provider), or this is a secondary Datacenter then
|
|
// the intermediate used to sign leaf certificates will be the last in the
|
|
// list.
|
|
IntermediateCerts []string
|
|
|
|
// SigningCert is the PEM-encoded signing certificate and SigningKey
|
|
// is the PEM-encoded private key for the signing certificate. These
|
|
// may actually be empty if the CA plugin in use manages these for us.
|
|
SigningCert string `json:",omitempty"`
|
|
SigningKey string `json:",omitempty"`
|
|
|
|
// Active is true if this is the current active CA. This must only
|
|
// be true for exactly one CA. For any method that modifies roots in the
|
|
// state store, tests should be written to verify that multiple roots
|
|
// cannot be active.
|
|
Active bool
|
|
|
|
// RotatedOutAt is the time at which this CA was removed from the state.
|
|
// This will only be set on roots that have been rotated out from being the
|
|
// active root.
|
|
RotatedOutAt time.Time `json:"-"`
|
|
|
|
// PrivateKeyType is the type of the private key used to sign certificates. It
|
|
// may be "rsa" or "ec". This is provided as a convenience to avoid parsing
|
|
// the public key to from the certificate to infer the type.
|
|
PrivateKeyType string
|
|
|
|
// PrivateKeyBits is the length of the private key used to sign certificates.
|
|
// This is provided as a convenience to avoid parsing the public key from the
|
|
// certificate to infer the type.
|
|
PrivateKeyBits int
|
|
|
|
RaftIndex
|
|
}
|
|
|
|
func (c *CARoot) Clone() *CARoot {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
|
|
newCopy := *c
|
|
newCopy.IntermediateCerts = CloneStringSlice(c.IntermediateCerts)
|
|
return &newCopy
|
|
}
|
|
|
|
// CARoots is a list of CARoot structures.
|
|
type CARoots []*CARoot
|
|
|
|
// Active returns the single CARoot that is marked as active, or nil if there
|
|
// is no active root (ex: when they are no roots).
|
|
func (c CARoots) Active() *CARoot {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
for _, r := range c {
|
|
if r.Active {
|
|
return r
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CASignRequest is the request for signing a service certificate.
|
|
type CASignRequest struct {
|
|
// Datacenter is the target for this request.
|
|
Datacenter string
|
|
|
|
// CSR is the PEM-encoded CSR.
|
|
CSR string
|
|
|
|
// WriteRequest is a common struct containing ACL tokens and other
|
|
// write-related common elements for requests.
|
|
WriteRequest
|
|
}
|
|
|
|
// RequestDatacenter returns the datacenter for a given request.
|
|
func (q *CASignRequest) RequestDatacenter() string {
|
|
return q.Datacenter
|
|
}
|
|
|
|
// IssuedCert is a certificate that has been issued by a Connect CA.
|
|
type IssuedCert struct {
|
|
// SerialNumber is the unique serial number for this certificate.
|
|
// This is encoded in standard hex separated by :.
|
|
SerialNumber string
|
|
|
|
// CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private
|
|
// key for that cert, respectively. This should not be stored in the
|
|
// state store, but is present in the sign API response.
|
|
CertPEM string `json:",omitempty"`
|
|
PrivateKeyPEM string `json:",omitempty"`
|
|
|
|
// Service is the name of the service for which the cert was issued.
|
|
// ServiceURI is the cert URI value.
|
|
Service string `json:",omitempty"`
|
|
ServiceURI string `json:",omitempty"`
|
|
|
|
// Agent is the name of the node for which the cert was issued.
|
|
// AgentURI is the cert URI value.
|
|
Agent string `json:",omitempty"`
|
|
AgentURI string `json:",omitempty"`
|
|
|
|
// ValidAfter and ValidBefore are the validity periods for the
|
|
// certificate.
|
|
ValidAfter time.Time
|
|
ValidBefore time.Time
|
|
|
|
// EnterpriseMeta is the Consul Enterprise specific metadata
|
|
EnterpriseMeta
|
|
|
|
RaftIndex
|
|
}
|
|
|
|
// CAOp is the operation for a request related to intentions.
|
|
type CAOp string
|
|
|
|
const (
|
|
CAOpSetRoots CAOp = "set-roots"
|
|
CAOpSetConfig CAOp = "set-config"
|
|
CAOpSetProviderState CAOp = "set-provider-state"
|
|
CAOpDeleteProviderState CAOp = "delete-provider-state"
|
|
CAOpSetRootsAndConfig CAOp = "set-roots-config"
|
|
CAOpIncrementProviderSerialNumber CAOp = "increment-provider-serial"
|
|
)
|
|
|
|
// CARequest is used to modify connect CA data. This is used by the
|
|
// FSM (agent/consul/fsm) to apply changes.
|
|
type CARequest struct {
|
|
// Op is the type of operation being requested. This determines what
|
|
// other fields are required.
|
|
Op CAOp
|
|
|
|
// Datacenter is the target for this request.
|
|
Datacenter string
|
|
|
|
// Index is used by CAOpSetRoots and CAOpSetConfig for a CAS operation.
|
|
Index uint64
|
|
|
|
// Roots is a list of roots. This is used for CAOpSet. One root must
|
|
// always be active.
|
|
Roots []*CARoot
|
|
|
|
// Config is the configuration for the current CA plugin.
|
|
Config *CAConfiguration
|
|
|
|
// ProviderState is the state for the builtin CA provider.
|
|
ProviderState *CAConsulProviderState
|
|
|
|
// WriteRequest is a common struct containing ACL tokens and other
|
|
// write-related common elements for requests.
|
|
WriteRequest
|
|
}
|
|
|
|
// RequestDatacenter returns the datacenter for a given request.
|
|
func (q *CARequest) RequestDatacenter() string {
|
|
return q.Datacenter
|
|
}
|
|
|
|
const (
|
|
ConsulCAProvider = "consul"
|
|
VaultCAProvider = "vault"
|
|
AWSCAProvider = "aws-pca"
|
|
)
|
|
|
|
// CAConfiguration is the configuration for the current CA plugin.
|
|
type CAConfiguration struct {
|
|
// ClusterID is a unique identifier for the cluster
|
|
ClusterID string `json:"-"`
|
|
|
|
// Provider is the CA provider implementation to use.
|
|
Provider string
|
|
|
|
// Configuration is arbitrary configuration for the provider. This
|
|
// should only contain primitive values and containers (such as lists
|
|
// and maps).
|
|
Config map[string]interface{}
|
|
|
|
// State is optionally used by the provider to persist information it needs
|
|
// between reloads like UUIDs of resources it manages. It only supports string
|
|
// values to avoid gotchas with interface{} since this is encoded through
|
|
// msgpack when it's written through raft. For example if providers used a
|
|
// custom struct or even a simple `int` type, msgpack with loose type
|
|
// information during encode/decode and providers will end up getting back
|
|
// different types have have to remember to test multiple variants of state
|
|
// handling to account for cases where it's been through msgpack or not.
|
|
// Keeping this as strings only forces compatibility and leaves the input
|
|
// Providers have to work with unambiguous - they can parse ints or other
|
|
// types as they need. We expect this only to be used to store a handful of
|
|
// identifiers anyway so this is simpler.
|
|
State map[string]string
|
|
|
|
// ForceWithoutCrossSigning indicates that the CA reconfiguration should go
|
|
// ahead even if the current CA is unable to cross sign certificates. This
|
|
// risks temporary connection failures during the rollout as new leafs will be
|
|
// rejected by proxies that have not yet observed the new root cert but is the
|
|
// only option if a CA that doesn't support cross signing needs to be
|
|
// reconfigured or mirated away from.
|
|
ForceWithoutCrossSigning bool
|
|
|
|
RaftIndex
|
|
}
|
|
|
|
func (c *CAConfiguration) UnmarshalJSON(data []byte) (err error) {
|
|
type Alias CAConfiguration
|
|
|
|
aux := &struct {
|
|
ForceWithoutCrossSigningSnake bool `json:"force_without_cross_signing"`
|
|
|
|
*Alias
|
|
}{
|
|
Alias: (*Alias)(c),
|
|
}
|
|
if err = lib.UnmarshalJSON(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
if aux.ForceWithoutCrossSigningSnake {
|
|
c.ForceWithoutCrossSigning = aux.ForceWithoutCrossSigningSnake
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *CAConfiguration) GetCommonConfig() (*CommonCAProviderConfig, error) {
|
|
if c == nil {
|
|
return nil, fmt.Errorf("config map was nil")
|
|
}
|
|
|
|
var config CommonCAProviderConfig
|
|
|
|
// Set Defaults
|
|
config.CSRMaxPerSecond = 50 // See doc comment for rationale here.
|
|
|
|
decodeConf := &mapstructure.DecoderConfig{
|
|
DecodeHook: ParseDurationFunc(),
|
|
Result: &config,
|
|
WeaklyTypedInput: true,
|
|
}
|
|
|
|
decoder, err := mapstructure.NewDecoder(decodeConf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := decoder.Decode(c.Config); err != nil {
|
|
return nil, fmt.Errorf("error decoding config: %s", err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
type CommonCAProviderConfig struct {
|
|
LeafCertTTL time.Duration
|
|
IntermediateCertTTL time.Duration
|
|
RootCertTTL time.Duration
|
|
|
|
SkipValidate bool
|
|
|
|
// CSRMaxPerSecond is a rate limit on processing Connect Certificate Signing
|
|
// Requests on the servers. It applies to all CA providers so can be used to
|
|
// limit rate to an external CA too. 0 disables the rate limit. Defaults to 50
|
|
// which is low enough to prevent overload of a reasonably sized production
|
|
// server while allowing a cluster with 1000 service instances to complete a
|
|
// rotation in 20 seconds. For reference a quad-core 2017 MacBook pro can
|
|
// process 100 signing RPCs a second while using less than half of one core.
|
|
// For large clusters with powerful servers it's advisable to increase this
|
|
// rate or to disable this limit and instead rely on CSRMaxConcurrent to only
|
|
// consume a subset of the server's cores.
|
|
CSRMaxPerSecond float32
|
|
|
|
// CSRMaxConcurrent is a limit on how many concurrent CSR signing requests
|
|
// will be processed in parallel. New incoming signing requests will try for
|
|
// `consul.csrSemaphoreWait` (currently 500ms) for a slot before being
|
|
// rejected with a "rate limited" backpressure response. This effectively sets
|
|
// how many CPU cores can be occupied by Connect CA signing activity and
|
|
// should be a (small) subset of your server's available cores to allow other
|
|
// tasks to complete when a barrage of CSRs come in (e.g. after a CA root
|
|
// rotation). Setting to 0 disables the limit, attempting to sign certs
|
|
// immediately in the RPC goroutine. This is 0 by default and CSRMaxPerSecond
|
|
// is used. This is ignored if CSRMaxPerSecond is non-zero.
|
|
CSRMaxConcurrent int
|
|
|
|
// PrivateKeyType specifies which type of key the CA should generate. It only
|
|
// applies when the provider is generating its own key and is ignored if the
|
|
// provider already has a key or an external key is provided. Supported values
|
|
// are "ec" or "rsa". "ec" is the default and will generate a NIST P-256
|
|
// Elliptic key.
|
|
PrivateKeyType string
|
|
|
|
// PrivateKeyBits specifies the number of bits the CA's private key should
|
|
// use. For RSA, supported values are 2048 and 4096. For EC, supported values
|
|
// are 224, 256, 384 and 521 and correspond to the NIST P-* curve of the same
|
|
// name. As with PrivateKeyType this is only relevant whan the provier is
|
|
// generating new CA keys (root or intermediate).
|
|
PrivateKeyBits int
|
|
}
|
|
|
|
var MinLeafCertTTL = time.Hour
|
|
var MaxLeafCertTTL = 365 * 24 * time.Hour
|
|
|
|
// intermediateCertRenewInterval is the interval at which the expiration
|
|
// of the intermediate cert is checked and renewed if necessary.
|
|
var IntermediateCertRenewInterval = time.Hour
|
|
|
|
func (c CommonCAProviderConfig) Validate() error {
|
|
if c.SkipValidate {
|
|
return nil
|
|
}
|
|
|
|
// it's sufficient to check that the root cert ttl >= intermediate cert ttl
|
|
// since intermediate cert ttl >= 3* leaf cert ttl; so root cert ttl >= 3 * leaf cert ttl > leaf cert ttl
|
|
if c.RootCertTTL < c.IntermediateCertTTL {
|
|
return fmt.Errorf("root cert TTL is set and is not greater than intermediate cert ttl. root cert ttl: %s, intermediate cert ttl: %s", c.RootCertTTL, c.IntermediateCertTTL)
|
|
}
|
|
|
|
if c.LeafCertTTL < MinLeafCertTTL {
|
|
return fmt.Errorf("leaf cert TTL must be greater or equal than %s", MinLeafCertTTL)
|
|
}
|
|
|
|
if c.LeafCertTTL > MaxLeafCertTTL {
|
|
return fmt.Errorf("leaf cert TTL must be less than %s", MaxLeafCertTTL)
|
|
}
|
|
|
|
if c.IntermediateCertTTL < (3 * IntermediateCertRenewInterval) {
|
|
// Intermediate Certificates are checked every
|
|
// hour(intermediateCertRenewInterval) if they are about to
|
|
// expire. Recreating an intermediate certs is started once
|
|
// more than half its lifetime has passed.
|
|
// If it would be 2h, worst case is that the check happens
|
|
// right before half time and when the check happens again, the
|
|
// certificate is very close to expiring, leaving only a small
|
|
// timeframe to renew. 3h leaves more than 30min to recreate.
|
|
// Right now the minimum LeafCertTTL is 1h, which means this
|
|
// check not strictly needed, because the same thing is covered
|
|
// in the next check too. But just in case minimum LeafCertTTL
|
|
// changes at some point, this validation must still be
|
|
// performed.
|
|
return fmt.Errorf("Intermediate Cert TTL must be greater or equal than %dh", 3*int(IntermediateCertRenewInterval.Hours()))
|
|
}
|
|
if c.IntermediateCertTTL < (3 * c.LeafCertTTL) {
|
|
// Intermediate Certificates are being sent to the proxy when
|
|
// the Leaf Certificate changes because they are bundled
|
|
// together.
|
|
// That means that the Intermediate Certificate TTL must be at
|
|
// a minimum of 3 * Leaf Certificate TTL to ensure that the new
|
|
// Intermediate is being set together with the Leaf Certificate
|
|
// before it expires.
|
|
return fmt.Errorf("Intermediate Cert TTL must be greater or equal than 3 * LeafCertTTL (>=%s).", 3*c.LeafCertTTL)
|
|
}
|
|
|
|
switch c.PrivateKeyType {
|
|
case "ec":
|
|
if c.PrivateKeyBits != 224 && c.PrivateKeyBits != 256 && c.PrivateKeyBits != 384 && c.PrivateKeyBits != 521 {
|
|
return fmt.Errorf("EC key length must be one of (224, 256, 384, 521) bits")
|
|
}
|
|
case "rsa":
|
|
if c.PrivateKeyBits != 2048 && c.PrivateKeyBits != 4096 {
|
|
return fmt.Errorf("RSA key length must be 2048 or 4096 bits")
|
|
}
|
|
default:
|
|
return fmt.Errorf("private key type must be either 'ec' or 'rsa'")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ConsulCAProviderConfig struct {
|
|
CommonCAProviderConfig `mapstructure:",squash"`
|
|
|
|
PrivateKey string
|
|
RootCert string
|
|
|
|
// DisableCrossSigning is really only useful in test code to use the built in
|
|
// provider while exercising logic that depends on the CA provider ability to
|
|
// cross sign. We don't document this config field publicly or make any
|
|
// attempt to parse it from snake case unlike other fields here.
|
|
DisableCrossSigning bool
|
|
}
|
|
|
|
func (c *ConsulCAProviderConfig) Validate() error {
|
|
return nil
|
|
}
|
|
|
|
// CAConsulProviderState is used to track the built-in Consul CA provider's state.
|
|
type CAConsulProviderState struct {
|
|
ID string
|
|
PrivateKey string
|
|
RootCert string
|
|
IntermediateCert string
|
|
|
|
RaftIndex
|
|
}
|
|
|
|
type VaultCAProviderConfig struct {
|
|
CommonCAProviderConfig `mapstructure:",squash"`
|
|
|
|
Address string
|
|
Token string
|
|
RootPKIPath string
|
|
IntermediatePKIPath string
|
|
Namespace string
|
|
|
|
CAFile string
|
|
CAPath string
|
|
CertFile string
|
|
KeyFile string
|
|
TLSServerName string
|
|
TLSSkipVerify bool
|
|
|
|
AuthMethod *VaultAuthMethod `alias:"auth_method"`
|
|
}
|
|
|
|
type VaultAuthMethod struct {
|
|
Type string
|
|
MountPath string `alias:"mount_path"`
|
|
Params map[string]interface{}
|
|
}
|
|
|
|
type AWSCAProviderConfig struct {
|
|
CommonCAProviderConfig `mapstructure:",squash"`
|
|
|
|
ExistingARN string
|
|
DeleteOnExit bool
|
|
}
|
|
|
|
// CALeafOp is the operation for a request related to leaf certificates.
|
|
type CALeafOp string
|
|
|
|
const (
|
|
CALeafOpIncrementIndex CALeafOp = "increment-index"
|
|
)
|
|
|
|
// CALeafRequest is used to modify connect CA leaf data. This is used by the
|
|
// FSM (agent/consul/fsm) to apply changes.
|
|
type CALeafRequest struct {
|
|
// Op is the type of operation being requested. This determines what
|
|
// other fields are required.
|
|
Op CALeafOp
|
|
|
|
// Datacenter is the target for this request.
|
|
Datacenter string
|
|
|
|
// WriteRequest is a common struct containing ACL tokens and other
|
|
// write-related common elements for requests.
|
|
WriteRequest
|
|
}
|
|
|
|
// RequestDatacenter returns the datacenter for a given request.
|
|
func (q *CALeafRequest) RequestDatacenter() string {
|
|
return q.Datacenter
|
|
}
|
|
|
|
// ParseDurationFunc is a mapstructure hook for decoding a string or
|
|
// []uint8 into a time.Duration value.
|
|
func ParseDurationFunc() mapstructure.DecodeHookFunc {
|
|
return func(
|
|
f reflect.Type,
|
|
t reflect.Type,
|
|
data interface{}) (interface{}, error) {
|
|
var v time.Duration
|
|
if t != reflect.TypeOf(v) {
|
|
return data, nil
|
|
}
|
|
|
|
switch {
|
|
case f.Kind() == reflect.String:
|
|
if dur, err := time.ParseDuration(data.(string)); err != nil {
|
|
return nil, err
|
|
} else {
|
|
v = dur
|
|
}
|
|
return v, nil
|
|
case f == reflect.SliceOf(reflect.TypeOf(uint8(0))):
|
|
s := Uint8ToString(data.([]uint8))
|
|
if dur, err := time.ParseDuration(s); err != nil {
|
|
return nil, err
|
|
} else {
|
|
v = dur
|
|
}
|
|
return v, nil
|
|
default:
|
|
return data, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func Uint8ToString(bs []uint8) string {
|
|
b := make([]byte, len(bs))
|
|
for i, v := range bs {
|
|
b[i] = v
|
|
}
|
|
return string(b)
|
|
}
|