mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
550 lines
17 KiB
550 lines
17 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
package structs |
|
|
|
import ( |
|
"encoding/base64" |
|
"fmt" |
|
"net/url" |
|
"time" |
|
|
|
"github.com/hashicorp/consul/acl" |
|
"github.com/hashicorp/go-multierror" |
|
) |
|
|
|
const ( |
|
DefaultClockSkewSeconds = 30 |
|
|
|
DiscoveryTypeStrictDNS ClusterDiscoveryType = "STRICT_DNS" |
|
DiscoveryTypeStatic ClusterDiscoveryType = "STATIC" |
|
DiscoveryTypeLogicalDNS ClusterDiscoveryType = "LOGICAL_DNS" |
|
DiscoveryTypeEDS ClusterDiscoveryType = "EDS" |
|
DiscoveryTypeOriginalDST ClusterDiscoveryType = "ORIGINAL_DST" |
|
) |
|
|
|
type JWTProviderConfigEntry struct { |
|
// Kind is the kind of configuration entry and must be "jwt-provider". |
|
Kind string `json:",omitempty"` |
|
|
|
// Name is the name of the provider being configured. |
|
Name string `json:",omitempty"` |
|
|
|
// JSONWebKeySet defines a JSON Web Key Set, its location on disk, or the |
|
// means with which to fetch a key set from a remote server. |
|
JSONWebKeySet *JSONWebKeySet `json:",omitempty" alias:"json_web_key_set"` |
|
|
|
// Issuer is the entity that must have issued the JWT. |
|
// This value must match the "iss" claim of the token. |
|
Issuer string `json:",omitempty"` |
|
|
|
// Audiences is the set of audiences the JWT is allowed to access. |
|
// If specified, all JWTs verified with this provider must address |
|
// at least one of these to be considered valid. |
|
Audiences []string `json:",omitempty"` |
|
|
|
// Locations where the JWT will be present in requests. |
|
// Envoy will check all of these locations to extract a JWT. |
|
// If no locations are specified Envoy will default to: |
|
// 1. Authorization header with Bearer schema: |
|
// "Authorization: Bearer <token>" |
|
// 2. access_token query parameter. |
|
Locations []*JWTLocation `json:",omitempty"` |
|
|
|
// Forwarding defines rules for forwarding verified JWTs to the backend. |
|
Forwarding *JWTForwardingConfig `json:",omitempty"` |
|
|
|
// ClockSkewSeconds specifies the maximum allowable time difference |
|
// from clock skew when validating the "exp" (Expiration) and "nbf" |
|
// (Not Before) claims. |
|
// |
|
// Default value is 30 seconds. |
|
ClockSkewSeconds int `json:",omitempty" alias:"clock_skew_seconds"` |
|
|
|
// CacheConfig defines configuration for caching the validation |
|
// result for previously seen JWTs. Caching results can speed up |
|
// verification when individual tokens are expected to be handled |
|
// multiple times. |
|
CacheConfig *JWTCacheConfig `json:",omitempty" alias:"cache_config"` |
|
|
|
Meta map[string]string `json:",omitempty"` |
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"` |
|
RaftIndex |
|
} |
|
|
|
// JWTLocation is a location where the JWT could be present in requests. |
|
// |
|
// Only one of Header, QueryParam, or Cookie can be specified. |
|
type JWTLocation struct { |
|
// Header defines how to extract a JWT from an HTTP request header. |
|
Header *JWTLocationHeader `json:",omitempty"` |
|
|
|
// QueryParam defines how to extract a JWT from an HTTP request |
|
// query parameter. |
|
QueryParam *JWTLocationQueryParam `json:",omitempty" alias:"query_param"` |
|
|
|
// Cookie defines how to extract a JWT from an HTTP request cookie. |
|
Cookie *JWTLocationCookie `json:",omitempty"` |
|
} |
|
|
|
func countTrue(vals ...bool) int { |
|
var result int |
|
for _, v := range vals { |
|
if v { |
|
result++ |
|
} |
|
} |
|
return result |
|
} |
|
|
|
func (location *JWTLocation) Validate() error { |
|
hasHeader := location.Header != nil |
|
hasQueryParam := location.QueryParam != nil |
|
hasCookie := location.Cookie != nil |
|
|
|
if countTrue(hasHeader, hasQueryParam, hasCookie) != 1 { |
|
return fmt.Errorf("must set exactly one of: JWT location header, query param or cookie") |
|
} |
|
|
|
if hasHeader { |
|
return location.Header.Validate() |
|
} |
|
|
|
if hasCookie { |
|
return location.Cookie.Validate() |
|
} |
|
|
|
return location.QueryParam.Validate() |
|
} |
|
|
|
// JWTLocationHeader defines how to extract a JWT from an HTTP |
|
// request header. |
|
type JWTLocationHeader struct { |
|
// Name is the name of the header containing the token. |
|
Name string `json:",omitempty"` |
|
|
|
// ValuePrefix is an optional prefix that precedes the token in the |
|
// header value. |
|
// For example, "Bearer " is a standard value prefix for a header named |
|
// "Authorization", but the prefix is not part of the token itself: |
|
// "Authorization: Bearer <token>" |
|
ValuePrefix string `json:",omitempty" alias:"value_prefix"` |
|
|
|
// Forward defines whether the header with the JWT should be |
|
// forwarded after the token has been verified. If false, the |
|
// header will not be forwarded to the backend. |
|
// |
|
// Default value is false. |
|
Forward bool `json:",omitempty"` |
|
} |
|
|
|
// JWTLocationQueryParam defines how to extract a JWT from an HTTP request query parameter. |
|
type JWTLocationQueryParam struct { |
|
// Name is the name of the query param containing the token. |
|
Name string `json:",omitempty"` |
|
} |
|
|
|
func (qp *JWTLocationQueryParam) Validate() error { |
|
if qp.Name == "" { |
|
return fmt.Errorf("JWT location query param name must be specified") |
|
} |
|
return nil |
|
} |
|
|
|
// JWTLocationCookie defines how to extract a JWT from an HTTP request cookie. |
|
type JWTLocationCookie struct { |
|
// Name is the name of the cookie containing the token. |
|
Name string `json:",omitempty"` |
|
} |
|
|
|
type JWTForwardingConfig struct { |
|
// HeaderName is a header name to use when forwarding a verified |
|
// JWT to the backend. The verified JWT could have been extracted |
|
// from any location (query param, header, or cookie). |
|
// |
|
// The header value will be base64-URL-encoded, and will not be |
|
// padded unless PadForwardPayloadHeader is true. |
|
HeaderName string `json:",omitempty" alias:"header_name"` |
|
|
|
// PadForwardPayloadHeader determines whether padding should be added |
|
// to the base64 encoded token forwarded with ForwardPayloadHeader. |
|
// |
|
// Default value is false. |
|
PadForwardPayloadHeader bool `alias:"pad_forward_payload_header"` |
|
} |
|
|
|
func (fc *JWTForwardingConfig) Validate() error { |
|
if fc.HeaderName == "" { |
|
return fmt.Errorf("Header name required for forwarding config") |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// JSONWebKeySet defines a key set, its location on disk, or the |
|
// means with which to fetch a key set from a remote server. |
|
// |
|
// Exactly one of Local or Remote must be specified. |
|
type JSONWebKeySet struct { |
|
// Local specifies a local source for the key set. |
|
Local *LocalJWKS `json:",omitempty"` |
|
|
|
// Remote specifies how to fetch a key set from a remote server. |
|
Remote *RemoteJWKS `json:",omitempty"` |
|
} |
|
|
|
// LocalJWKS specifies a location for a local JWKS. |
|
// |
|
// Only one of String and Filename can be specified. |
|
type LocalJWKS struct { |
|
// JWKS contains a base64 encoded JWKS. |
|
JWKS string `json:",omitempty"` |
|
|
|
// Filename configures a location on disk where the JWKS can be |
|
// found. If specified, the file must be present on the disk of ALL |
|
// proxies with intentions referencing this provider. |
|
Filename string `json:",omitempty"` |
|
} |
|
|
|
func (ks *LocalJWKS) Validate() error { |
|
hasFilename := ks.Filename != "" |
|
hasJWKS := ks.JWKS != "" |
|
|
|
if countTrue(hasFilename, hasJWKS) != 1 { |
|
return fmt.Errorf("must specify exactly one of String or filename for local keyset") |
|
} |
|
|
|
if hasJWKS { |
|
if _, err := base64.StdEncoding.DecodeString(ks.JWKS); err != nil { |
|
return fmt.Errorf("JWKS must be valid base64 encoded string") |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// RemoteJWKS specifies how to fetch a JWKS from a remote server. |
|
type RemoteJWKS struct { |
|
// URI is the URI of the server to query for the JWKS. |
|
URI string `json:",omitempty"` |
|
|
|
// RequestTimeoutMs is the number of milliseconds to |
|
// time out when making a request for the JWKS. |
|
RequestTimeoutMs int `json:",omitempty" alias:"request_timeout_ms"` |
|
|
|
// CacheDuration is the duration after which cached keys |
|
// should be expired. |
|
// |
|
// Default value from envoy is 10 minutes. |
|
CacheDuration time.Duration `json:",omitempty" alias:"cache_duration"` |
|
|
|
// FetchAsynchronously indicates that the JWKS should be fetched |
|
// when a client request arrives. Client requests will be paused |
|
// until the JWKS is fetched. |
|
// If false, the proxy listener will wait for the JWKS to be |
|
// fetched before being activated. |
|
// |
|
// Default value is false. |
|
FetchAsynchronously bool `json:",omitempty" alias:"fetch_asynchronously"` |
|
|
|
// RetryPolicy defines a retry policy for fetching JWKS. |
|
// |
|
// There is no retry by default. |
|
RetryPolicy *JWKSRetryPolicy `json:",omitempty" alias:"retry_policy"` |
|
|
|
// JWKSCluster defines how the specified Remote JWKS URI is to be fetched. |
|
JWKSCluster *JWKSCluster `json:",omitempty" alias:"jwks_cluster"` |
|
} |
|
|
|
func (ks *RemoteJWKS) Validate() error { |
|
if ks.URI == "" { |
|
return fmt.Errorf("Remote JWKS URI is required") |
|
} |
|
|
|
if _, err := url.ParseRequestURI(ks.URI); err != nil { |
|
return fmt.Errorf("Remote JWKS URI is invalid: %w, uri: %s", err, ks.URI) |
|
} |
|
|
|
if ks.RetryPolicy != nil && ks.RetryPolicy.RetryPolicyBackOff != nil { |
|
err := ks.RetryPolicy.RetryPolicyBackOff.Validate() |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
|
|
if ks.JWKSCluster != nil { |
|
return ks.JWKSCluster.Validate() |
|
} |
|
|
|
return nil |
|
} |
|
|
|
type JWKSCluster struct { |
|
// DiscoveryType refers to the service discovery type to use for resolving the cluster. |
|
// |
|
// This defaults to STRICT_DNS. |
|
// Other options include STATIC, LOGICAL_DNS, EDS or ORIGINAL_DST. |
|
DiscoveryType ClusterDiscoveryType `json:",omitempty" alias:"discovery_type"` |
|
|
|
// TLSCertificates refers to the data containing certificate authority certificates to use |
|
// in verifying a presented peer certificate. |
|
// If not specified and a peer certificate is presented it will not be verified. |
|
// |
|
// Must be either CaCertificateProviderInstance or TrustedCA. |
|
TLSCertificates *JWKSTLSCertificate `json:",omitempty" alias:"tls_certificates"` |
|
|
|
// The timeout for new network connections to hosts in the cluster. |
|
// If not set, a default value of 5s will be used. |
|
ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"` |
|
} |
|
|
|
type ClusterDiscoveryType string |
|
|
|
func (d ClusterDiscoveryType) Validate() error { |
|
switch d { |
|
case DiscoveryTypeStatic, DiscoveryTypeStrictDNS, DiscoveryTypeLogicalDNS, DiscoveryTypeEDS, DiscoveryTypeOriginalDST: |
|
return nil |
|
default: |
|
return fmt.Errorf("unsupported jwks cluster discovery type: %q", d) |
|
} |
|
} |
|
|
|
func (c *JWKSCluster) Validate() error { |
|
if c.DiscoveryType != "" { |
|
err := c.DiscoveryType.Validate() |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
|
|
if c.TLSCertificates != nil { |
|
return c.TLSCertificates.Validate() |
|
} |
|
return nil |
|
} |
|
|
|
// JWKSTLSCertificate refers to the data containing certificate authority certificates to use |
|
// in verifying a presented peer certificate. |
|
// If not specified and a peer certificate is presented it will not be verified. |
|
// |
|
// Must be either CaCertificateProviderInstance or TrustedCA. |
|
type JWKSTLSCertificate struct { |
|
// CaCertificateProviderInstance Certificate provider instance for fetching TLS certificates. |
|
CaCertificateProviderInstance *JWKSTLSCertProviderInstance `json:",omitempty" alias:"ca_certificate_provider_instance"` |
|
|
|
// TrustedCA defines TLS certificate data containing certificate authority certificates |
|
// to use in verifying a presented peer certificate. |
|
// |
|
// Exactly one of Filename, EnvironmentVariable, InlineString or InlineBytes must be specified. |
|
TrustedCA *JWKSTLSCertTrustedCA `json:",omitempty" alias:"trusted_ca"` |
|
} |
|
|
|
func (c *JWKSTLSCertificate) Validate() error { |
|
hasProviderInstance := c.CaCertificateProviderInstance != nil |
|
hasTrustedCA := c.TrustedCA != nil |
|
|
|
if countTrue(hasProviderInstance, hasTrustedCA) != 1 { |
|
return fmt.Errorf("must specify exactly one of: CaCertificateProviderInstance or TrustedCA for JKWS' TLSCertificates") |
|
} |
|
|
|
if c.TrustedCA != nil { |
|
return c.TrustedCA.Validate() |
|
} |
|
return nil |
|
} |
|
|
|
type JWKSTLSCertProviderInstance struct { |
|
// InstanceName refers to the certificate provider instance name |
|
// |
|
// The default value is "default". |
|
InstanceName string `json:",omitempty" alias:"instance_name"` |
|
|
|
// CertificateName is used to specify certificate instances or types. For example, "ROOTCA" to specify |
|
// a root-certificate (validation context) or "example.com" to specify a certificate for a |
|
// particular domain. |
|
// |
|
// The default value is the empty string. |
|
CertificateName string `json:",omitempty" alias:"certificate_name"` |
|
} |
|
|
|
// JWKSTLSCertTrustedCA defines TLS certificate data containing certificate authority certificates |
|
// to use in verifying a presented peer certificate. |
|
// |
|
// Exactly one of Filename, EnvironmentVariable, InlineString or InlineBytes must be specified. |
|
type JWKSTLSCertTrustedCA struct { |
|
Filename string `json:",omitempty" alias:"filename"` |
|
EnvironmentVariable string `json:",omitempty" alias:"environment_variable"` |
|
InlineString string `json:",omitempty" alias:"inline_string"` |
|
InlineBytes []byte `json:",omitempty" alias:"inline_bytes"` |
|
} |
|
|
|
func (c *JWKSTLSCertTrustedCA) Validate() error { |
|
hasFilename := c.Filename != "" |
|
hasEnv := c.EnvironmentVariable != "" |
|
hasInlineBytes := len(c.InlineBytes) > 0 |
|
hasInlineString := c.InlineString != "" |
|
|
|
if countTrue(hasFilename, hasEnv, hasInlineString, hasInlineBytes) != 1 { |
|
return fmt.Errorf("must specify exactly one of: Filename, EnvironmentVariable, InlineString or InlineBytes for JWKS' TrustedCA") |
|
} |
|
return nil |
|
} |
|
|
|
type JWKSRetryPolicy struct { |
|
// NumRetries is the number of times to retry fetching the JWKS. |
|
// The retry strategy uses jittered exponential backoff with |
|
// a base interval of 1s and max of 10s. |
|
// |
|
// Default value is 0. |
|
NumRetries int `json:",omitempty" alias:"num_retries"` |
|
|
|
// Backoff policy |
|
// |
|
// Defaults to envoy's backoff policy |
|
RetryPolicyBackOff *RetryPolicyBackOff `json:",omitempty" alias:"retry_policy_back_off"` |
|
} |
|
|
|
type RetryPolicyBackOff struct { |
|
// BaseInterval to be used for the next back off computation |
|
// |
|
// The default value from envoy is 1s |
|
BaseInterval time.Duration `json:",omitempty" alias:"base_interval"` |
|
|
|
// MaxInternal to be used to specify the maximum interval between retries. |
|
// Optional but should be greater or equal to BaseInterval. |
|
// |
|
// Defaults to 10 times BaseInterval |
|
MaxInterval time.Duration `json:",omitempty" alias:"max_interval"` |
|
} |
|
|
|
func (r *RetryPolicyBackOff) Validate() error { |
|
|
|
if (r.MaxInterval != 0) && (r.BaseInterval > r.MaxInterval) { |
|
return fmt.Errorf("retry policy backoff's MaxInterval should be greater or equal to BaseInterval") |
|
} |
|
|
|
return nil |
|
} |
|
|
|
type JWTCacheConfig struct { |
|
// Size specifies the maximum number of JWT verification |
|
// results to cache. |
|
// |
|
// Defaults to 0, meaning that JWT caching is disabled. |
|
Size int `json:",omitempty"` |
|
} |
|
|
|
func (e *JWTProviderConfigEntry) GetKind() string { return JWTProvider } |
|
func (e *JWTProviderConfigEntry) GetName() string { return e.Name } |
|
func (e *JWTProviderConfigEntry) GetMeta() map[string]string { return e.Meta } |
|
func (e *JWTProviderConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta } |
|
func (e *JWTProviderConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex } |
|
|
|
func (e *JWTProviderConfigEntry) CanRead(authz acl.Authorizer) error { |
|
var authzContext acl.AuthorizerContext |
|
e.FillAuthzContext(&authzContext) |
|
|
|
// allow service-identity tokens the ability to read jwt-providers |
|
// this is a workaround to allow sidecar proxies to read the jwt-providers |
|
// see issue: https://github.com/hashicorp/consul/issues/17886 for more details |
|
err := authz.ToAllowAuthorizer().ServiceWriteAnyAllowed(&authzContext) |
|
if err == nil { |
|
return err |
|
} |
|
|
|
return authz.ToAllowAuthorizer().MeshReadAllowed(&authzContext) |
|
} |
|
|
|
func (e *JWTProviderConfigEntry) CanWrite(authz acl.Authorizer) error { |
|
var authzContext acl.AuthorizerContext |
|
e.FillAuthzContext(&authzContext) |
|
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext) |
|
} |
|
|
|
func (jwks *JSONWebKeySet) Validate() error { |
|
hasLocalKeySet := jwks.Local != nil |
|
hasRemoteKeySet := jwks.Remote != nil |
|
|
|
if countTrue(hasLocalKeySet, hasRemoteKeySet) != 1 { |
|
return fmt.Errorf("must specify exactly one of Local or Remote JSON Web key set") |
|
} |
|
|
|
if hasRemoteKeySet { |
|
return jwks.Remote.Validate() |
|
} |
|
|
|
return jwks.Local.Validate() |
|
} |
|
|
|
func (lh *JWTLocationHeader) Validate() error { |
|
if lh.Name == "" { |
|
return fmt.Errorf("JWT location header name must be specified") |
|
} |
|
return nil |
|
} |
|
|
|
func (lc *JWTLocationCookie) Validate() error { |
|
if lc.Name == "" { |
|
return fmt.Errorf("JWT location cookie name must be specified") |
|
} |
|
return nil |
|
} |
|
|
|
func validateLocations(locations []*JWTLocation) error { |
|
var result error |
|
for _, location := range locations { |
|
if err := location.Validate(); err != nil { |
|
result = multierror.Append(result, err) |
|
} |
|
} |
|
return result |
|
} |
|
|
|
func (e *JWTProviderConfigEntry) Validate() error { |
|
if e.Name == "" { |
|
return fmt.Errorf("Name is required") |
|
} |
|
|
|
if err := validateConfigEntryMeta(e.Meta); err != nil { |
|
return err |
|
} |
|
|
|
if err := e.validatePartition(); err != nil { |
|
return err |
|
} |
|
|
|
if e.JSONWebKeySet == nil { |
|
return fmt.Errorf("JSONWebKeySet is required") |
|
} |
|
|
|
if err := e.JSONWebKeySet.Validate(); err != nil { |
|
return err |
|
} |
|
|
|
if err := validateLocations(e.Locations); err != nil { |
|
return err |
|
} |
|
|
|
if e.Forwarding != nil { |
|
if err := e.Forwarding.Validate(); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func (e *JWTProviderConfigEntry) Normalize() error { |
|
if e == nil { |
|
return fmt.Errorf("Config entry is nil") |
|
} |
|
|
|
e.Kind = JWTProvider |
|
e.EnterpriseMeta.Normalize() |
|
|
|
if e.ClockSkewSeconds == 0 { |
|
e.ClockSkewSeconds = DefaultClockSkewSeconds |
|
} |
|
|
|
return nil |
|
}
|
|
|