diff --git a/api/acl.go b/api/acl.go index 6f81c6ae3..c391e26e7 100644 --- a/api/acl.go +++ b/api/acl.go @@ -827,6 +827,7 @@ type ACLAuthMethodConfig struct { // The OAuth Client Secret configured with the OIDC provider OIDCClientSecret string // Optionally send a signed JWT ("private key jwt") as a client assertion + // to the OIDC provider OIDCClientAssertion *OIDCClientAssertion // Disable S256 PKCE challenge verification OIDCDisablePKCE *bool @@ -972,35 +973,35 @@ const ( // OIDCClientAssertion (a.k.a private_key_jwt) is used to send // a client_assertion along with an OIDC token request. +// Reference: https://oauth.net/private-key-jwt/ // See also: structs.OIDCClientAssertion type OIDCClientAssertion struct { + // Audience is/are who will be processing the assertion. + // Defaults to the parent `ACLAuthMethodConfig`'s `OIDCDiscoveryURL` + Audience []string + // KeySource is where to get the private key to sign the JWT. // It is the one field that *must* be set to enable client assertions. // Available sources: - // * "nomad" = Use current active key in Nomad's keyring - // * "private_key" = Use key material in the PrivateKey field of this struct - // * "client_secret" = Use the OIDCClientSecret inherited from the parent - // ACLAuthMethodConfig struct + // - "nomad": Use current active key in Nomad's keyring + // - "private_key": Use key material in the `PrivateKey` field + // - "client_secret": Use the `OIDCClientSecret` inherited from the parent + // `ACLAuthMethodConfig` as an HMAC key KeySource OIDCClientAssertionKeySource - // Audience is/are who will be processing the assertion. - // Defaults to the parent ACLAuthMethodConfig's OIDCDiscoveryURL - Audience []string - - // PrivateKey contains external key material provided by users. - // KeySource must be "private_key" to enable this. - PrivateKey *OIDCClientAssertionKey - // KeyAlgorithm is the key's algorithm. - // Its default values are based on the KeySource: - // * nomad = "RS256" -- pulled from the keyring - // * private_key = "RS256" - // * client_secret = "HS256" - // Only RSA algorithms are supported for nomad and private_key. + // Its default values are based on the `KeySource`: + // - "nomad": "RS256" (from Nomad's keyring, must not be changed) + // - "private_key": "RS256" (must be RS256, RS384, or RS512) + // - "client_secret": "HS256" (must be HS256, HS384, or HS512) KeyAlgorithm string + // PrivateKey contains external key material provided by users. + // `KeySource` must be "private_key" to enable this. + PrivateKey *OIDCClientAssertionKey + // ExtraHeaders are added to the JWT headers, alongside "kid" and "type" - // Setting the "kid" header here is not allowed; use PrivateKey.KeyID. + // Setting the "kid" header here is not allowed; use `PrivateKey.KeyID`. ExtraHeaders map[string]string } @@ -1026,47 +1027,43 @@ const ( // PemKeyFile and PemCertFile, if set, must be an absolute path to a file // present on disk on any Nomad servers that may become cluster leaders. type OIDCClientAssertionKey struct { - // PemKey is the private key, in pem format. It is used to sign the JWT. - // Mutually exclusive with PemKeyFile. + // PemKey is an RSA private key, in pem format. It is used to sign the JWT. + // Mutually exclusive with `PemKeyFile`. PemKey string // PemKeyFile is an absolute path to a private key on Nomad servers' disk, // in pem format. It is used to sign the JWT. - // Mutually exclusive with PemKey. + // Mutually exclusive with `PemKey`. PemKeyFile string - // KeyIDHeader is which header to set for they provider to identify the - // public key to use to verify the signed JWT. Its default values vary + // KeyIDHeader is which header the provider will use to find the + // public key to verify the signed JWT. Its default values vary // based on which of the other required fields is set: - // KeyID: "kid" - // PemCert: "x5t#S256" - // PemCertFile: "x5t#S256" + // - KeyID: "kid" + // - PemCert: "x5t#S256" + // - PemCertFile: "x5t#S256" // - // Valid values are: "kid", "x5t", "x5t#S256" - // If "x5t" is selected, Nomad uses sha1 to derive the x5t header - // from the provided certificate. - // - // Refer to the RFC for more information on JWT key headers: - // "kid": https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4 - // "x5t": https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.7 - // "x5t#S256": https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.8 + // Refer to the JWS RFC for information on these headers: + // - "kid": https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4 + // - "x5t": https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.7 + // - "x5t#S256": https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.8 // // If you need to set some other header not supported here, // you may use OIDCClientAssertion.ExtraHeaders. KeyIDHeader OIDCClientAssertionKeyIDHeader // KeyID may be set manually and becomes the "kid" header. - // Mutually exclusive with PemCert and PemCertFile. - // Allowed KeyIDHeader values: "kid" (the default) + // Mutually exclusive with `PemCert` and `PemCertFile`. + // Allowed `KeyIDHeader` values: "kid" (the default) KeyID string // PemCert is an x509 certificate, signed by the private key or a CA, - // in pem format. It is used to derive an x5t#S256 (or x5t) KeyID. - // Mutually exclusive with PemCertFile and KeyID. - // Allowed KeyIDHeader values: "x5t", "x5t#S256" (default "x5t#S256") + // in pem format. It is used to derive an x5t#S256 (or x5t) header. + // Mutually exclusive with `PemCertFile` and `KeyID`. + // Allowed `KeyIDHeader` values: "x5t", "x5t#S256" (default "x5t#S256") PemCert string // PemCertFile is an absolute path to an x509 certificate on Nomad servers' // disk, signed by the private key or a CA, in pem format. - // It is used to derive an x5t#S256 (or x5t) KeyID. - // Mutually exclusive with PemCert and KeyID. - // Allowed KeyIDHeader values: "x5t", "x5t#S256" (default "x5t#S256") + // It is used to derive an x5t#S256 (or x5t) header. + // Mutually exclusive with `PemCert` and `KeyID`. + // Allowed `KeyIDHeader` values: "x5t", "x5t#S256" (default "x5t#S256") PemCertFile string } diff --git a/lib/auth/oidc/client_assertion.go b/lib/auth/oidc/client_assertion.go index 099a9453e..33f6f0885 100644 --- a/lib/auth/oidc/client_assertion.go +++ b/lib/auth/oidc/client_assertion.go @@ -29,6 +29,25 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) +// BuildClientAssertionJWT makes a JWT to be included in an OIDC auth request. +// Reference: https://oauth.net/private-key-jwt/ +// +// There are three input variations depending on OIDCClientAssertion.KeySource: +// - "client_secret": uses the config's ClientSecret as an HMAC key to sign +// the JWT. This is marginally more secure than a bare ClientSecret, as the +// JWT is time-bound, and signed by the secret rather than sending the +// secret itself over the network. +// - "nomad": uses the RS256 nomadKey (Nomad's private key) to sign the JWT, +// and the nomadKID as the JWT's "kid" header, which the OIDC provider uses +// to find the public key at Nomad's JWKS endpoint (/.well-known/jwks.json) +// to verify the JWT signature. This is arguably the most secure option, +// because only Nomad has the private key. +// - "private_key": uses an RSA private key provided by the user. They may +// provide a KeyID to use as the JWT's "kid" header, or an x509 public +// certificate to derive an x5t#S256 (or x5t) header, which the OIDC +// provider uses to find the cert on their end to verify the JWT signature. +// This is the most flexible option, allowing users to manage their own +// keys however they like. func BuildClientAssertionJWT(config *structs.ACLAuthMethodConfig, nomadKey *rsa.PrivateKey, nomadKID string) (*cass.JWT, error) { // should already be validated by caller, but just in case. if config == nil || config.OIDCClientAssertion == nil { diff --git a/nomad/structs/acl.go b/nomad/structs/acl.go index 6a7fddee9..d037e001e 100644 --- a/nomad/structs/acl.go +++ b/nomad/structs/acl.go @@ -995,16 +995,16 @@ func (a *ACLAuthMethod) Validate(minTTL, maxTTL time.Duration) error { mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type)) } + if err := a.Config.Validate(a.Type); err != nil { + mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid config: %w", err)) + } + if minTTL > a.MaxTokenTTL || a.MaxTokenTTL > maxTTL { mErr.Errors = append(mErr.Errors, fmt.Errorf( "invalid MaxTokenTTL value '%s' (should be between %s and %s)", a.MaxTokenTTL.String(), minTTL.String(), maxTTL.String())) } - if err := a.Config.Validate(); err != nil { - mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid config: %w", err)) - } - return mErr.ErrorOrNil() } @@ -1121,28 +1121,36 @@ func (a *ACLAuthMethodConfig) Canonicalize() { if len(a.OIDCClientAssertion.Audience) == 0 { a.OIDCClientAssertion.Audience = []string{a.OIDCDiscoveryURL} } + // move the client secret into the client assertion a.OIDCClientAssertion.ClientSecret = a.OIDCClientSecret + // do not also send the client secret normally + a.OIDCClientSecret = "" a.OIDCClientAssertion.Canonicalize() } } -func (a *ACLAuthMethodConfig) Validate() error { +func (a *ACLAuthMethodConfig) Validate(methodType string) error { if a == nil { return errors.New("missing auth method Config") } mErr := &multierror.Error{} - if a.OIDCDiscoveryURL == "" { - mErr = multierror.Append(mErr, errors.New("missing OIDCDiscoveryURL")) - } - if a.OIDCClientID == "" { - mErr = multierror.Append(mErr, errors.New("missing OIDCClientID")) - } - if len(a.BoundAudiences) == 0 || a.BoundAudiences[0] == "" { - mErr = multierror.Append(mErr, errors.New("missing BoundAudiences")) - } - if err := a.OIDCClientAssertion.Validate(); err != nil { - mErr = multierror.Append(mErr, fmt.Errorf("invalid client assertion config: %w", err)) + + switch methodType { + case ACLAuthMethodTypeOIDC: + if a.OIDCDiscoveryURL == "" { + mErr = multierror.Append(mErr, errors.New("missing OIDCDiscoveryURL")) + } + if a.OIDCClientID == "" { + mErr = multierror.Append(mErr, errors.New("missing OIDCClientID")) + } + if err := a.OIDCClientAssertion.Validate(); err != nil { + mErr = multierror.Append(mErr, fmt.Errorf("invalid client assertion config: %w", err)) + } + + case ACLAuthMethodTypeJWT: + // TODO: check JWT fields: https://hashicorp.atlassian.net/browse/NET-12309 } + return helper.FlattenMultierror(mErr) } diff --git a/nomad/structs/acl_test.go b/nomad/structs/acl_test.go index ff5b3b547..7a9d4b76e 100644 --- a/nomad/structs/acl_test.go +++ b/nomad/structs/acl_test.go @@ -1392,20 +1392,37 @@ func TestACLAuthMethod_Merge(t *testing.T) { must.Eq(t, am1.Config.OIDCClientAssertion.PrivateKey.KeyID, "test-key-id") } +func TestACLAuthMethodConfig_Canonicalize(t *testing.T) { + // client assertions get some defaults from their parent + cass := &OIDCClientAssertion{} + am := &ACLAuthMethodConfig{ + OIDCDiscoveryURL: "test-disco-url", + OIDCClientSecret: "super secret", + OIDCClientAssertion: cass, // not nil + } + am.Canonicalize() + must.Eq(t, []string{"test-disco-url"}, cass.Audience, must.Sprint("should inherit audience")) + must.Eq(t, "super secret", cass.ClientSecret, must.Sprint("should inherit secret")) + must.Eq(t, "", am.OIDCClientSecret, must.Sprint("secret should move to assertion")) +} + func TestACLAuthMethodConfig_Validate(t *testing.T) { ci.Parallel(t) // fail everything am := &ACLAuthMethodConfig{} am.Canonicalize() // there is insufficient info for this to help - err := am.Validate() + err := am.Validate("OIDC") must.ErrorContains(t, err, "missing OIDCDiscoveryURL") must.ErrorContains(t, err, "missing OIDCClientID") - must.ErrorContains(t, err, "missing BoundAudiences") // TODO: BoundIssuer? it's not even documented... am.OIDCClientAssertion = &OIDCClientAssertion{} am.Canonicalize() - err = am.Validate() + err = am.Validate("OIDC") must.ErrorContains(t, err, "invalid client assertion config:") + + // do not fail, because no JWT validation at the moment + err = am.Validate("JWT") + must.NoError(t, err) } func TestACLAuthMethodConfig_Copy(t *testing.T) { diff --git a/website/content/api-docs/acl/auth-methods.mdx b/website/content/api-docs/acl/auth-methods.mdx index 42d03ca09..9584c443e 100644 --- a/website/content/api-docs/acl/auth-methods.mdx +++ b/website/content/api-docs/acl/auth-methods.mdx @@ -27,98 +27,7 @@ The table below shows this endpoint's support for ### Parameters -- `Name` `(string: )` - Name is the identifier of the ACL auth method. - The name can contain alphanumeric characters and dashes. This name must be - unique and must not exceed 128 characters. - -- `Type` `(string: )` - ACL auth method type, supports `OIDC` and `JWT`. - -- `TokenLocality` `(string: )` - Defines whether the ACL auth method - creates a local or global token when performing SSO login. This field must be - set to either `local` or `global`. - -- `TokenNameFormat` `(string )` - Defines the token name format for the - generated tokens This can be lightly templated using HIL `${foo}` syntax. - Defaults to `${auth_method_type}-${auth_method_name}`. - - - `MaxTokenTTL` `(duration: )` - Defines the maximum life of a token created - by this method. When set, it will initialize the `ExpirationTime` field on all - tokens to a value of `Token.CreateTime + AuthMethod.MaxTokenTTL`. This field is - not persisted beyond its initial use. Can be specified in the form of `"60s"` or - `"5m"` (i.e., 60 seconds or 5 minutes, respectively). - -- `Default` `(bool: false)` - Defines whether this ACL Auth Method is to be - set as default when running `nomad login` command. - -- `Config` `(ACLAuthMethodConfig: )` - The raw configuration to use for - the auth method. This parameter is part of the auth method configuration, not - specific to Nomad. - - - `OIDCDiscoveryURL` `(string: )` - The OIDC discovery URL, without - any `.well-known` component (base path). Required for `OIDC` method type. - Either this, the `JWKSURL` or the `JWTValidationPubKeys` is required for - `JWT` method type. - - - `OIDCClientID` `(string: )` - The OAuth client ID configured with - your OIDC provider. Required for `OIDC` method type. - - - `OIDCClientSecret` `(string: )` - The OAuth client secret - configured with your OIDC provider. Required for `OIDC` method type. - - - `OIDCDisableUserInfo` `(bool: false)` - When set to `true`, Nomad will not make - a request to the identity provider to get OIDC UserInfo. You may wish to set this - if your identity provider doesn't send any additional claims from the UserInfo - endpoint. - - - `OIDCScopes` `(array)` - List of OIDC scopes. - - - `JWTValidationPubKeys` `(array)` - A list of PEM-encoded public keys - to use to validate JWT signatures locally. Either this, the `JWKSURL` or the - `OIDCDiscoveryURL` is required for `JWT` method type. - - - `JWKSURL` `(string)` - JSON Web Key Sets url for authenticating JWT - signatures. Either this, the `JWTValidationPubKeys` or the - `OIDCDiscoverURL` is required for `JWT` method type. - - - `BoundAudiences` `(array)` - List of aud claims that are valid for - login; any match is sufficient. - - - `AllowedRedirectURIs` `(array)` - A list of allowed values for - redirect_uri. Must be non-empty. - - - `DiscoveryCaPem` `(array)` - PEM encoded CA certs for use by the TLS - client used to talk with the OIDC discovery URL. If not set, system - certificates are used. - - - `JWKSCACert` `(string)` - PEM encoded CA cert for use by the TLS client used - to talk with the JWKS server. - - - `SigningAlgs` `(array)` - A list of supported signing algorithms. - Defaults to `RS256`. - - - `ExpirationLeeway` `(duration)` - Duration in seconds of leeway when - validating expiration of a JWT to account for clock skew. - - - `NotBeforeLeeway` `(duration)` - Duration in seconds of leeway when - validating not before values of a JWT to account for clock skew. - - - `ClockSkewLeeway` `(duration)` - Duration in seconds of leeway when - validating all JWT claims to account for clock skew. - - - `ClaimMappings` `(map[string]string)` - Mappings of claims (key) that will - be copied to a metadata field (value). Use this if the claim you are capturing - is singular (such as an attribute). - - When mapped, the values in each list can be any of a number, string, or - boolean and will all be stringified when returned. - - - `ListClaimMappings` `(map[string]string)` - Mappings of claims (key) will be - copied to a metadata field (value). Use this if the claim you are capturing is - list-like (such as groups). - - - `VerboseLogging` `(bool: false)` - When set to `true`, Nomad will log token claims - and information related to binding-rule and role/policy evaluations. Not recommended - in production since sensitive information may be present. +@include 'api-docs/auth-method-params.mdx' ### Sample payload @@ -177,7 +86,7 @@ $ curl \ "Config": { "OIDCDiscoveryURL": "https://my-corp-app-name.auth0.com/", "OIDCClientID": "v1rpi2myptmv1rpi2myptmv1rpi2mypt", - "OIDCClientSecret": "example-client-secret", + "OIDCClientSecret": "redacted", "OIDCScopes": [ "groups" ], @@ -222,73 +131,9 @@ queries](/nomad/api-docs#blocking-queries) and [required ACLs](/nomad/api-docs#a ### Parameters -- `Name` `(string: )` - Names is the identifier of the ACL auth - method. The name can contain alphanumeric characters and dashes. - This name must be unique and must not exceed 128 characters. +The parameters are the same as Create. -- `Type` `(string: )` - ACL auth role SSO identifier. Currently, the - only supported Type is "OIDC." - -- `TokenLocality` `(string: "")` - Defines whether the ACL auth method - creates a local or global token when performing SSO login. This field must be - set to either "local" or "global" - -- `TokenNameFormat` `(string )` - Defines the token name format for the - generated tokens This can be lightly templated using HIL '${foo}' syntax. - Defaults to '${auth_method_type}-${auth_method_name}' - -- `MaxTokenTTL` `(duration: )` - Defines the maximum life of a token created - by this method. When set it will initialize the `ExpirationTime` field on all - tokens to a value of `Token.CreateTime + AuthMethod.MaxTokenTTL`. This field is - not persisted beyond its initial use. Can be specified in the form of `"60s"` or - `"5m"` (i.e., 60 seconds or 5 minutes, respectively). - -- `Default` `(bool: false)` - Defines whether this ACL auth method is to be - set as default when running `nomad login` command. - -- `Config` `(ACLAuthMethodConfig: nil)` - The raw configuration to use for - the auth method. This parameter is part of the auth method configuration, not - specific to Nomad. - - - `OIDCDiscoveryURL` `(string: "")` - The OIDC discovery URL, without - any .well-known component (base path). - - - `OIDCClientID` `(string: "")` - The OAuth client ID configured with - your OIDC provider. - - - `OIDCClientSecret` `(string: "")` - The OAuth client secret - configured with your OIDC provider. - - - `OIDCDisableUserInfo` `(bool: false)` - When set to `true`, Nomad will not make - a request to the identity provider to get OIDC UserInfo. You may wish to set this - if your identity provider doesn't send any additional claims from the UserInfo - endpoint. - - - `OIDCScopes` `(array)` - List of OIDC scopes. - - - `BoundAudiences` `(array)` - List of aud claims that are valid for - login; any match is sufficient. - - - `AllowedRedirectURIs` `(array)` - A list of allowed values for - redirect_uri. Must be non-empty. - - - `DiscoveryCaPem` `(array)` - PEM encoded CA certs for use by the TLS - client used to talk with the OIDC discovery URL. If not set, system - certificates are used. - - - `SigningAlgs` `(array)` - A list of supported signing algorithms. - Defaults to `RS256`. - - - `ClaimMappings` `(map[string]string)` - Mappings of claims (key) that will - be copied to a metadata field (value). Use this if the claim you are capturing - is singular (such as an attribute). - - When mapped, the values in each list can be any of a number, string, or - boolean and will all be stringified when returned. - - - `ListClaimMappings` `(map[string]string)` - Mappings of claims (key) will be - copied to a metadata field (value). Use this if the claim you are capturing is - list-like (such as groups). +@include 'api-docs/auth-method-params.mdx' ### Sample Payload @@ -347,7 +192,7 @@ $ curl \ "Config": { "OIDCDiscoveryURL": "https://my-corp-app-name.auth0.com/", "OIDCClientID": "V1RPi2MYptMV1RPi2MYptMV1RPi2MYpt", - "OIDCClientSecret": "example-client-secret", + "OIDCClientSecret": "redacted", "OIDCScopes": [ "groups" ], @@ -512,3 +357,9 @@ $ curl \ --header "X-Nomad-Token: " \ https://localhost:4646/v1/acl/auth-method/example-acl-auth-method ``` + +[private key jwt]: https://oauth.net/private-key-jwt/ +[concepts-assertions]: /nomad/docs/concepts/acl/auth-methods/oidc#client-assertions +[x5t]: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.7 +[x5t#S256]: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.8 +[pkce]: https://oauth.net/2/pkce/ diff --git a/website/content/docs/commands/acl/auth-method/create.mdx b/website/content/docs/commands/acl/auth-method/create.mdx index 6abcc0827..7841e3fae 100644 --- a/website/content/docs/commands/acl/auth-method/create.mdx +++ b/website/content/docs/commands/acl/auth-method/create.mdx @@ -45,9 +45,9 @@ via flags detailed below. - `-default`: Specifies whether this auth method should be treated as a default one in case no auth method is explicitly specified for a login command. -- `-config`: Auth method [configuration] in JSON format. May be prefixed with '@' - to indicate that the value is a file path to load the config from. '-' may also - be given to indicate that the config is available on stdin. +- `-config`: Auth method [configuration][] in JSON format. You may provide '-' + to send the config through stdin, or prefix a file path with '@' to indicate + that the config should be loaded from the file. - `-json`: Output the ACL auth-method in a JSON format. @@ -104,4 +104,27 @@ Example config file: } ``` +This example config uses a private key JWT [client assertion][] +instead of a client secret. + +```json +{ + "OIDCDiscoveryURL": "https://my-keycloak-instance.com/realms/nomad", + "OIDCClientID": "my-great-client-id", + "OIDCClientAssertion": { + "KeySource": "nomad" + }, + "BoundAudiences": [ + "my-great-client-id" + ], + "AllowedRedirectURIs": [ + "http://localhost:4646/oidc/callback" + ], + "ListClaimMappings": { + "groups": "groups" + } +} +``` + [configuration]: /nomad/api-docs/acl/auth-methods#config +[client assertion]: /nomad/api-docs/acl/auth-methods#oidcclientassertion diff --git a/website/content/docs/commands/acl/auth-method/update.mdx b/website/content/docs/commands/acl/auth-method/update.mdx index 7e2952aea..10311c28f 100644 --- a/website/content/docs/commands/acl/auth-method/update.mdx +++ b/website/content/docs/commands/acl/auth-method/update.mdx @@ -39,7 +39,8 @@ The `acl auth-method update` command requires an existing method's name. to the command. Instead, overwrite all fields with the exception of the role ID which is immutable. -- `-type`: Updates the type of the auth method. Supported types are `OIDC` and `JWT`. +- `-type`: Updates the type of the auth method. Supported types are `OIDC` and + `JWT`. - `-max-token-ttl`: Updates the duration of time all tokens created by this auth method should be valid for. @@ -54,9 +55,9 @@ The `acl auth-method update` command requires an existing method's name. - `-default`: Specifies whether this auth method should be treated as a default one in case no auth method is explicitly specified for a login command. -- `-config`: Auth method [configuration] in JSON format. May be prefixed with '@' - to indicate that the value is a file path to load the config from. '-' may also - be given to indicate that the config is available on stdin. +- `-config`: Auth method [configuration][] in JSON format. You may provide '-' + to send the config through stdin, or prefix a file path with '@' to indicate + that the config should be loaded from the file. - `-json`: Output the ACL auth method in a JSON format. @@ -89,3 +90,5 @@ Signing algorithms = Claim mappings = {http://example.com/first_name: first_name}; {http://example.com/last_name: last_name} List claim mappings = {http://nomad.com/groups: groups} ``` + +[configuration]: /nomad/api-docs/acl/auth-methods#config diff --git a/website/content/docs/concepts/acl/auth-methods/oidc.mdx b/website/content/docs/concepts/acl/auth-methods/oidc.mdx index 6968abb08..910c1cecb 100644 --- a/website/content/docs/concepts/acl/auth-methods/oidc.mdx +++ b/website/content/docs/concepts/acl/auth-methods/oidc.mdx @@ -74,6 +74,117 @@ Complete the login via your OIDC provider. Launching browser to: Your browser opens to the generated URL to complete the provider's login. Enter the URL manually if the browser does not automatically open. +### Client assertions + +Also known as "[private key JWT][]", client assertions offer a more secure +authentication mechanism compared to client secrets. + +Instead of sending a simple secret, Nomad builds a JWT and signs it with +an RSA private key (or HMAC) that the OIDC provider can verify with an +associated public key (or the same HMAC). In this way, Nomad "asserts" +that it is a valid OIDC client without sending any secret information over +the network. + +Here are some partial [auth method configuration][] examples. They focus only +on the client assertion feature; they are not complete, functional examples. + +#### Nomad keyring + +In this example for Keycloak, Nomad signs the JWT with its own internal private +key. It sets the JWT's "kid" header as the key ID, as presented by Nomad's +[jwks.json][] endpoint. + +This is arguably the most secure option, because only Nomad has the private key. + +```json +{ + "OIDCDiscoveryURL": "https://your-keycloak-instance.com/realms/nomad", + "OIDCClientID": "{your-client-id}", + "BoundAudiences": ["{your-client-id}"], + "OIDCClientAssertion": { + "Audience": ["https://your-keycloak-instance.com/realms/nomad"], + "KeySource": "nomad" + } +} +``` + +Notice the distinction between the two "audience" fields: +* `BoundAudiences` is often the application client ID (Nomad being the client), + which Nomad verifies against what the OIDC provider sends to Nomad. Nomad + uses this to make sure that requests are for Nomad, and not some other client. + This applies to all OIDC configuration, not only client assertions. +* `OIDCClientAssertion.Audience` is the OIDC provider, because that is the + target audience of the client assertion JWT. The provider uses this to make + sure that requests are for it and not some other provider. This is often the + same as the `OIDCDiscoveryURL`, so defaults to that. This applies to all + client assertion configuration. + +This option requires that the OIDC provider have network access to Nomad's JWKS, +either directly or via proxy, but otherwise requires no extra management of key +material beyond Nomad's built-in [keyring][key-management]. + +#### User provided key + +This Microsoft Entra ID (formerly Azure Active Directory) example uses an RSA +private key generated separately from Nomad to sign the JWT. +- The `PemKey` value is the private RSA key contents in PEM format. +- The `PemCert` value is the contents of an X509 certificate from the key or a + CA. Nomad uses this certificate to derive an [x5t#S256][] thumbprint header. + +```json +{ + "OIDCDiscoveryURL": "https://login.microsoftonline.com/{tenant}/v2.0", + "OIDCClientID": "{app-client-id}", + "BoundAudiences": ["{app-client-id}"], + "OIDCClientAssertion": { + "KeySource": "private_key", + "KeyAlgorithm": "RS256", + "PrivateKey": { + "PemKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIE...the-rest-of-the-key...uJ8fR\n-----END RSA PRIVATE KEY-----", + "PemCert": "-----BEGIN CERTIFICATE-----\nMIID...the-rest-of-the-cert...GUCk=\n-----END CERTIFICATE-----" + } + } +} +``` + +Note that if you implement this approach, you must upload the certificate to +the Entra ID app, so that when you try to log in, Entra ID can use the +"x5t#S256" header to look up the public key that it has stored. + +You may also configure the key and/or certificate as filenames on disk on Nomad +servers with the `PemKeyFile` and `PemCertFile` options, respectively. This +approach lets you rotate your key/cert without needing to update the auth +method, but the files must be present on the disk of any server that may become +Nomad leader. + +Or, depending on your OIDC provider's requirements, you may provide the `KeyID` +directly, instead of providing a certificate. + +This approach lets you bring your own RSA key for the following scenarios: +- Your OIDC provider does not support JWKS +- Your network topology does not allow connectivity between the provider and +Nomad JWKS, even via proxy +- You want a signing key that is specifically and only for this purpose. + +#### Client secret HMAC + +This example uses the `OIDCClientSecret` as an HMAC key to sign the JWT. +This configuration is marginally more secure than a bare client secret, as the +JWT is time-bound, and signed by the secret rather than sending the secret +itself over the network. As with a normal client secret, both Nomad and the +OIDC provider need to have the same secret. + +```json +{ + "OIDCDiscoveryURL": "https://your-oidc-provider.com/oidc-discovery-url", + "OIDCClientID": "your-client-id", + "OIDCClientSecret": "long-secret-id-has-to-be-at-least-32-bytes", + "OIDCClientAssertion": { + "KeySource": "client_secret" + } +} +``` + ## OIDC Configuration Troubleshooting The amount of configuration required for OIDC is relatively small, but it can @@ -110,8 +221,19 @@ port numbers, and whether trailing slashes are present. what you expect. Since claims data is logged verbatim and may contain sensitive information, do not use this option in production. +- For client assertions, if `VerboseLogging` is enabled, then the Nomad leader + server logs a JWT when the auth method is created, and when someone makes + a login attempt. These JWTs are not 100% identical to what gets sent to the + OIDC provider due to being time-bound, but you can check the JWT headers + and claims to compare with your OIDC provider's requirements. + @include 'jwt_claim_mapping_details.mdx' [ACL Overview]: /nomad/docs/concepts/acl [auth-method create]: /nomad/docs/commands/acl/auth-method/create +[private key jwt]: https://oauth.net/private-key-jwt/ +[auth method configuration]: /nomad/api-docs/acl/auth-methods +[key-management]: /nomad/docs/operations/key-management +[x5t#S256]: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.8 +[jwks.json]: /nomad/api-docs/operator/keyring#list-active-public-keys [VerboseLogging]: /nomad/api-docs/acl/auth-methods#verboselogging diff --git a/website/content/docs/operations/key-management.mdx b/website/content/docs/operations/key-management.mdx index 6b8142125..b98d24825 100644 --- a/website/content/docs/operations/key-management.mdx +++ b/website/content/docs/operations/key-management.mdx @@ -6,9 +6,10 @@ description: Learn about the key management in Nomad. # Key Management -Nomad servers maintain an encryption keyring used to encrypt [Variables][] and -sign task [workload identities][]. The servers encrypt these data encryption -keys (DEK) and store the wrapped keys in Raft. +Nomad servers maintain an encryption keyring used to encrypt [Variables][], +sign task [workload identities][], and sign OIDC [client assertion JWTs][]. +The servers encrypt these data encryption keys (DEK) and store the wrapped keys +in Raft. The key encryption key (KEK) used to encrypt the DEK is controlled by the [`keyring`][] provider. When using an external KMS or Vault transit encryption @@ -89,6 +90,7 @@ keyring rotate`][] once the servers have joined. [variables]: /nomad/docs/concepts/variables [workload identities]: /nomad/docs/concepts/workload-identity +[client assertion JWTs]: /nomad/docs/concepts/acl/auth-methods/oidc#client-assertions [data directory]: /nomad/docs/configuration#data_dir [`keyring`]: /nomad/docs/configuration/keyring [`nomad operator root keyring rotate -full`]: /nomad/docs/commands/operator/root/keyring-rotate diff --git a/website/content/docs/upgrade/upgrade-specific.mdx b/website/content/docs/upgrade/upgrade-specific.mdx index c7818b2a6..9cfbb1a23 100644 --- a/website/content/docs/upgrade/upgrade-specific.mdx +++ b/website/content/docs/upgrade/upgrade-specific.mdx @@ -87,6 +87,22 @@ Nomad no longer creates an implicit Consul identity for workloads that don't register services with Consul. Tasks that require Consul tokens for template rendering must include a [`consul` block][] or specify an [`identity`][]. +#### OIDC login with PKCE +Nomad now enables +[Proof Key for Code Exchange (PKCE)](https://oauth.net/2/pkce/) +by default for new or updated OIDC auth methods. Existing auth methods remain +unaffected until changed using the +[acl auth-method create](/nomad/docs/commands/acl/auth-method/create) or +[acl auth-method update](/nomad/docs/commands/acl/auth-method/update) +CLI commands, or with the +[auth method API](/nomad/api-docs/acl/auth-methods) directly. + +Set the [`OIDCDisablePKCE`](/nomad/api-docs/acl/auth-methods#oidcdisablepkce) +option to turn off this extra security. + +Note that although Nomad enables PKCE by default, some OIDC providers may +require you to also enable it in their configuration. + ## Nomad 1.9.5 #### CNI plugins diff --git a/website/content/partials/api-docs/auth-method-params.mdx b/website/content/partials/api-docs/auth-method-params.mdx new file mode 100644 index 000000000..4052139e1 --- /dev/null +++ b/website/content/partials/api-docs/auth-method-params.mdx @@ -0,0 +1,163 @@ + + +- `Name` `(string: )` - Name is the identifier of the ACL auth method. +The name can contain alphanumeric characters and dashes. This name must be +unique and must not exceed 128 characters. + +- `Type` `(string: )` - ACL auth method type, supports `OIDC` and +`JWT`. + +- `TokenLocality` `(string: )` - Defines whether the ACL auth method +creates a local or global token when performing SSO login. This field must be +set to either `local` or `global`. + +- `TokenNameFormat` `(string )` - Defines the token name format for +the generated tokens This can be lightly templated using HIL `${foo}` syntax. +Defaults to `${auth_method_type}-${auth_method_name}`. + + - `MaxTokenTTL` `(duration: )` - Defines the maximum life of a token +created by this method. When set, it will initialize the `ExpirationTime` field +on all tokens to a value of `Token.CreateTime + AuthMethod.MaxTokenTTL`. This +field is not persisted beyond its initial use. Can be specified in the form of +`"60s"` or `"5m"` (i.e., 60 seconds or 5 minutes, respectively). + +- `Default` `(bool: false)` - Defines whether this ACL Auth Method is to be +set as default when running `nomad login` command. + +- `Config` `(ACLAuthMethodConfig: )` - The raw configuration to use +for the auth method. + + - `OIDCDiscoveryURL` `(string: )` - The OIDC discovery URL, without + any `.well-known` component (base path). Required for `OIDC` method type. + Either this, the `JWKSURL` or the `JWTValidationPubKeys` is required for + `JWT` method type. + + - `OIDCClientID` `(string: )` - The OAuth client ID configured with + your OIDC provider. Required for `OIDC` method type. + + - `OIDCClientSecret` `(string: )` - The OAuth client secret + configured with your OIDC provider. Required for `OIDC` method type. + + - `OIDCClientAssertion` `(OIDCClientAssertion)` - Optionally send a signed + JWT ("[private key jwt][]") as a client assertion to the OIDC provider. + Browse to the [OIDC concepts][concepts-assertions] page to learn more. + + - `Audience` `(array)` - Who processes the assertion. + Defaults to the parent `ACLAuthMethodConfig`'s `OIDCDiscoveryURL` + + - `KeySource` `(string: )` - Specifies where to get the private + key to sign the JWT. + Available sources: + - "nomad": Use current active key in Nomad's keyring + - "private_key": Use key material in the `PrivateKey` field + - "client_secret": Use the `OIDCClientSecret` inherited from the parent + `ACLAuthMethodConfig` as an HMAC key + + - `KeyAlgorithm` `(string)` is the key's algorithm. Its default values are + based on the `KeySource`: + - "nomad": "RS256"; this is from Nomad's keyring and must not be changed + - "private_key": "RS256"; must be RS256, RS384, or RS512 + - "client_secret": "HS256"; must be HS256, HS384, or HS512 + + - `PrivateKey` `(OIDCClientAssertionKey)` - External key material to sign + the JWT. `KeySource` must be "private_key" to enable this. + + - `PemKey` `(string)` - An RSA private key, in pem format. It is used to + sign the JWT. Mutually exclusive with `PemKeyFile`. + + - `PemKeyFile` `(string)` - An absolute path to a private key on Nomad + servers' disk, in pem format. It is used to sign the JWT. + Mutually exclusive with `PemKey`. + + - `KeyIDHeader` `(string)` - Which header the provider uses to find + the public key to verify the signed JWT. + The default and allowed values depend on whether you set `KeyID`, + `PemCert`, or `PemCertFile`. You must set exactly one of those options, + so refer to them for their requirements. + + - `KeyID` `(string)` - Becomes the JWT's "kid" header. + Mutually exclusive with `PemCert` and `PemCertFile`. + Allowed `KeyIDHeader` values: "kid" (the default) + + - `PemCert` `(string)` - An x509 certificate, signed by the private key + or a CA, in pem format. Nomad uses this certificate to derive an + [x5t#S256][] (or [x5t][]) KeyID. + Mutually exclusive with `PemCertFile` and `KeyID`. + Allowed `KeyIDHeader` values: "x5t", "x5t#S256" (default "x5t#S256") + + - `PemCertFile` `(string)` - An absolute path to an x509 certificate on + Nomad servers' disk, signed by the private key or a CA, in pem format. + Nomad uses this certificate to derive an [x5t#S256][] (or [x5t][]) + header. Mutually exclusive with `PemCert` and KeyID. + Allowed `KeyIDHeader` values: "x5t", "x5t#S256" (default "x5t#S256") + + - `ExtraHeaders` `(map[string]string)` - Added to the JWT headers, + alongside "kid" and "type". Setting the "kid" header here is not allowed; + use `PrivateKey.KeyID`. + + - `OIDCDisablePKCE` `(bool: false)` - When set to `true`, Nomad will not + include [PKCE][] verification in the auth flow. Even with PKCE enabled in + Nomad, which is the default setting, you may still need to enable it in the + OIDC provider. + + - `OIDCDisableUserInfo` `(bool: false)` - When set to `true`, Nomad will not + make a request to the identity provider to get OIDC UserInfo. You may wish to + set this if your identity provider doesn't send any additional claims from the + UserInfo endpoint. + + - `OIDCScopes` `(array)` - List of OIDC scopes. + + - `JWTValidationPubKeys` `(array)` - A list of PEM-encoded public keys + to use to validate JWT signatures locally. Either this, the `JWKSURL` or the + `OIDCDiscoveryURL` is required for `JWT` method type. + + - `JWKSURL` `(string)` - JSON Web Key Sets url for authenticating JWT + signatures. Either this, the `JWTValidationPubKeys` or the + `OIDCDiscoverURL` is required for `JWT` method type. + + - `BoundAudiences` `(array)` - List of aud claims that are valid for + login; any match is sufficient. + + - `BoundIssuer` `(array)` - The value against which to match the iss + claim in a JWT. + + - `AllowedRedirectURIs` `(array)` - A list of allowed values for + redirect_uri. Must be non-empty. + + - `DiscoveryCaPem` `(array)` - PEM encoded CA certs for use by the TLS + client used to talk with the OIDC discovery URL. If not set, system + certificates are used. + + - `JWKSCACert` `(string)` - PEM encoded CA cert for use by the TLS client used + to talk with the JWKS server. + + - `SigningAlgs` `(array)` - A list of supported signing algorithms. + Defaults to `RS256`. + + - `ExpirationLeeway` `(duration)` - Duration in seconds of leeway when + validating expiration of a JWT to account for clock skew. + + - `NotBeforeLeeway` `(duration)` - Duration in seconds of leeway when + validating not before values of a JWT to account for clock skew. + + - `ClockSkewLeeway` `(duration)` - Duration in seconds of leeway when + validating all JWT claims to account for clock skew. + + - `ClaimMappings` `(map[string]string)` - Mappings of claims (key) that will + be copied to a metadata field (value). Use this if the claim you are capturing + is singular (such as an attribute). + + When mapped, the values in each list can be any of a number, string, or + boolean and will all be stringified when returned. + + - `ListClaimMappings` `(map[string]string)` - Mappings of claims (key) will be + copied to a metadata field (value). Use this if the claim you are capturing is + list-like (such as groups). + + - `VerboseLogging` `(bool: false)` - When set to `true`, Nomad will log token + claims, information related to binding-rule and role/policy evaluations, + and client assertion JWTs, if applicable. Not recommended in production, + since sensitive information may be present.