/ *
Copyright 2016 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package options
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/egressselector"
genericoptions "k8s.io/apiserver/pkg/server/options"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/klog/v2"
openapicommon "k8s.io/kube-openapi/pkg/common"
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
"k8s.io/kubernetes/pkg/features"
kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap"
)
type BuiltInAuthenticationOptions struct {
APIAudiences [ ] string
Anonymous * AnonymousAuthenticationOptions
BootstrapToken * BootstrapTokenAuthenticationOptions
ClientCert * genericoptions . ClientCertAuthenticationOptions
OIDC * OIDCAuthenticationOptions
RequestHeader * genericoptions . RequestHeaderAuthenticationOptions
ServiceAccounts * ServiceAccountAuthenticationOptions
TokenFile * TokenFileAuthenticationOptions
WebHook * WebHookAuthenticationOptions
TokenSuccessCacheTTL time . Duration
TokenFailureCacheTTL time . Duration
}
type AnonymousAuthenticationOptions struct {
Allow bool
}
type BootstrapTokenAuthenticationOptions struct {
Enable bool
}
type OIDCAuthenticationOptions struct {
CAFile string
ClientID string
IssuerURL string
UsernameClaim string
UsernamePrefix string
GroupsClaim string
GroupsPrefix string
SigningAlgs [ ] string
RequiredClaims map [ string ] string
}
type ServiceAccountAuthenticationOptions struct {
KeyFiles [ ] string
Lookup bool
Issuer string
JWKSURI string
MaxExpiration time . Duration
ExtendExpiration bool
}
type TokenFileAuthenticationOptions struct {
TokenFile string
}
type WebHookAuthenticationOptions struct {
ConfigFile string
Version string
CacheTTL time . Duration
}
func NewBuiltInAuthenticationOptions ( ) * BuiltInAuthenticationOptions {
return & BuiltInAuthenticationOptions {
TokenSuccessCacheTTL : 10 * time . Second ,
TokenFailureCacheTTL : 0 * time . Second ,
}
}
func ( s * BuiltInAuthenticationOptions ) WithAll ( ) * BuiltInAuthenticationOptions {
return s .
WithAnonymous ( ) .
WithBootstrapToken ( ) .
WithClientCert ( ) .
WithOIDC ( ) .
WithRequestHeader ( ) .
WithServiceAccounts ( ) .
WithTokenFile ( ) .
WithWebHook ( )
}
func ( s * BuiltInAuthenticationOptions ) WithAnonymous ( ) * BuiltInAuthenticationOptions {
s . Anonymous = & AnonymousAuthenticationOptions { Allow : true }
return s
}
func ( s * BuiltInAuthenticationOptions ) WithBootstrapToken ( ) * BuiltInAuthenticationOptions {
s . BootstrapToken = & BootstrapTokenAuthenticationOptions { }
return s
}
func ( s * BuiltInAuthenticationOptions ) WithClientCert ( ) * BuiltInAuthenticationOptions {
s . ClientCert = & genericoptions . ClientCertAuthenticationOptions { }
return s
}
func ( s * BuiltInAuthenticationOptions ) WithOIDC ( ) * BuiltInAuthenticationOptions {
s . OIDC = & OIDCAuthenticationOptions { }
return s
}
func ( s * BuiltInAuthenticationOptions ) WithRequestHeader ( ) * BuiltInAuthenticationOptions {
s . RequestHeader = & genericoptions . RequestHeaderAuthenticationOptions { }
return s
}
func ( s * BuiltInAuthenticationOptions ) WithServiceAccounts ( ) * BuiltInAuthenticationOptions {
s . ServiceAccounts = & ServiceAccountAuthenticationOptions { Lookup : true }
return s
}
func ( s * BuiltInAuthenticationOptions ) WithTokenFile ( ) * BuiltInAuthenticationOptions {
s . TokenFile = & TokenFileAuthenticationOptions { }
return s
}
func ( s * BuiltInAuthenticationOptions ) WithWebHook ( ) * BuiltInAuthenticationOptions {
s . WebHook = & WebHookAuthenticationOptions {
Version : "v1beta1" ,
CacheTTL : 2 * time . Minute ,
}
return s
}
// Validate checks invalid config combination
func ( s * BuiltInAuthenticationOptions ) Validate ( ) [ ] error {
allErrors := [ ] error { }
if s . OIDC != nil && ( len ( s . OIDC . IssuerURL ) > 0 ) != ( len ( s . OIDC . ClientID ) > 0 ) {
allErrors = append ( allErrors , fmt . Errorf ( "oidc-issuer-url and oidc-client-id should be specified together" ) )
}
if s . ServiceAccounts != nil && len ( s . ServiceAccounts . Issuer ) > 0 && strings . Contains ( s . ServiceAccounts . Issuer , ":" ) {
if _ , err := url . Parse ( s . ServiceAccounts . Issuer ) ; err != nil {
allErrors = append ( allErrors , fmt . Errorf ( "service-account-issuer contained a ':' but was not a valid URL: %v" , err ) )
}
}
if s . ServiceAccounts != nil && utilfeature . DefaultFeatureGate . Enabled ( features . BoundServiceAccountTokenVolume ) {
if ! utilfeature . DefaultFeatureGate . Enabled ( features . TokenRequest ) || ! utilfeature . DefaultFeatureGate . Enabled ( features . TokenRequestProjection ) {
allErrors = append ( allErrors , errors . New ( "if the BoundServiceAccountTokenVolume feature is enabled," +
" the TokenRequest and TokenRequestProjection features must also be enabled" ) )
}
if len ( s . ServiceAccounts . Issuer ) == 0 {
allErrors = append ( allErrors , errors . New ( "service-account-issuer is a required flag when BoundServiceAccountTokenVolume is enabled" ) )
}
if len ( s . ServiceAccounts . KeyFiles ) == 0 {
allErrors = append ( allErrors , errors . New ( "service-account-key-file is a required flag when BoundServiceAccountTokenVolume is enabled" ) )
}
}
if s . ServiceAccounts != nil {
if utilfeature . DefaultFeatureGate . Enabled ( features . ServiceAccountIssuerDiscovery ) {
// Validate the JWKS URI when it is explicitly set.
// When unset, it is later derived from ExternalHost.
if s . ServiceAccounts . JWKSURI != "" {
if u , err := url . Parse ( s . ServiceAccounts . JWKSURI ) ; err != nil {
allErrors = append ( allErrors , fmt . Errorf ( "service-account-jwks-uri must be a valid URL: %v" , err ) )
} else if u . Scheme != "https" {
allErrors = append ( allErrors , fmt . Errorf ( "service-account-jwks-uri requires https scheme, parsed as: %v" , u . String ( ) ) )
}
}
} else if len ( s . ServiceAccounts . JWKSURI ) > 0 {
allErrors = append ( allErrors , fmt . Errorf ( "service-account-jwks-uri may only be set when the ServiceAccountIssuerDiscovery feature gate is enabled" ) )
}
}
return allErrors
}
func ( s * BuiltInAuthenticationOptions ) AddFlags ( fs * pflag . FlagSet ) {
fs . StringSliceVar ( & s . APIAudiences , "api-audiences" , s . APIAudiences , "" +
"Identifiers of the API. The service account token authenticator will validate that " +
"tokens used against the API are bound to at least one of these audiences. If the " +
"--service-account-issuer flag is configured and this flag is not, this field " +
"defaults to a single element list containing the issuer URL." )
if s . Anonymous != nil {
fs . BoolVar ( & s . Anonymous . Allow , "anonymous-auth" , s . Anonymous . Allow , "" +
"Enables anonymous requests to the secure port of the API server. " +
"Requests that are not rejected by another authentication method are treated as anonymous requests. " +
"Anonymous requests have a username of system:anonymous, and a group name of system:unauthenticated." )
}
if s . BootstrapToken != nil {
fs . BoolVar ( & s . BootstrapToken . Enable , "enable-bootstrap-token-auth" , s . BootstrapToken . Enable , "" +
"Enable to allow secrets of type 'bootstrap.kubernetes.io/token' in the 'kube-system' " +
"namespace to be used for TLS bootstrapping authentication." )
}
if s . ClientCert != nil {
s . ClientCert . AddFlags ( fs )
}
if s . OIDC != nil {
fs . StringVar ( & s . OIDC . IssuerURL , "oidc-issuer-url" , s . OIDC . IssuerURL , "" +
"The URL of the OpenID issuer, only HTTPS scheme will be accepted. " +
"If set, it will be used to verify the OIDC JSON Web Token (JWT)." )
fs . StringVar ( & s . OIDC . ClientID , "oidc-client-id" , s . OIDC . ClientID ,
"The client ID for the OpenID Connect client, must be set if oidc-issuer-url is set." )
fs . StringVar ( & s . OIDC . CAFile , "oidc-ca-file" , s . OIDC . CAFile , "" +
"If set, the OpenID server's certificate will be verified by one of the authorities " +
"in the oidc-ca-file, otherwise the host's root CA set will be used." )
fs . StringVar ( & s . OIDC . UsernameClaim , "oidc-username-claim" , "sub" , "" +
"The OpenID claim to use as the user name. Note that claims other than the default ('sub') " +
"is not guaranteed to be unique and immutable. This flag is experimental, please see " +
"the authentication documentation for further details." )
fs . StringVar ( & s . OIDC . UsernamePrefix , "oidc-username-prefix" , "" , "" +
"If provided, all usernames will be prefixed with this value. If not provided, " +
"username claims other than 'email' are prefixed by the issuer URL to avoid " +
"clashes. To skip any prefixing, provide the value '-'." )
fs . StringVar ( & s . OIDC . GroupsClaim , "oidc-groups-claim" , "" , "" +
"If provided, the name of a custom OpenID Connect claim for specifying user groups. " +
"The claim value is expected to be a string or array of strings. This flag is experimental, " +
"please see the authentication documentation for further details." )
fs . StringVar ( & s . OIDC . GroupsPrefix , "oidc-groups-prefix" , "" , "" +
"If provided, all groups will be prefixed with this value to prevent conflicts with " +
"other authentication strategies." )
fs . StringSliceVar ( & s . OIDC . SigningAlgs , "oidc-signing-algs" , [ ] string { "RS256" } , "" +
"Comma-separated list of allowed JOSE asymmetric signing algorithms. JWTs with a " +
"'alg' header value not in this list will be rejected. " +
"Values are defined by RFC 7518 https://tools.ietf.org/html/rfc7518#section-3.1." )
fs . Var ( cliflag . NewMapStringStringNoSplit ( & s . OIDC . RequiredClaims ) , "oidc-required-claim" , "" +
"A key=value pair that describes a required claim in the ID Token. " +
"If set, the claim is verified to be present in the ID Token with a matching value. " +
"Repeat this flag to specify multiple claims." )
}
if s . RequestHeader != nil {
s . RequestHeader . AddFlags ( fs )
}
if s . ServiceAccounts != nil {
fs . StringArrayVar ( & s . ServiceAccounts . KeyFiles , "service-account-key-file" , s . ServiceAccounts . KeyFiles , "" +
"File containing PEM-encoded x509 RSA or ECDSA private or public keys, used to verify " +
"ServiceAccount tokens. The specified file can contain multiple keys, and the flag can " +
"be specified multiple times with different files. If unspecified, " +
"--tls-private-key-file is used. Must be specified when " +
"--service-account-signing-key is provided" )
fs . BoolVar ( & s . ServiceAccounts . Lookup , "service-account-lookup" , s . ServiceAccounts . Lookup ,
"If true, validate ServiceAccount tokens exist in etcd as part of authentication." )
fs . StringVar ( & s . ServiceAccounts . Issuer , "service-account-issuer" , s . ServiceAccounts . Issuer , "" +
"Identifier of the service account token issuer. The issuer will assert this identifier " +
"in \"iss\" claim of issued tokens. This value is a string or URI. If this option is not " +
"a valid URI per the OpenID Discovery 1.0 spec, the ServiceAccountIssuerDiscovery feature " +
"will remain disabled, even if the feature gate is set to true. It is highly recommended " +
"that this value comply with the OpenID spec: https://openid.net/specs/openid-connect-discovery-1_0.html. " +
"In practice, this means that service-account-issuer must be an https URL. It is also highly " +
"recommended that this URL be capable of serving OpenID discovery documents at " +
"`{service-account-issuer}/.well-known/openid-configuration`." )
fs . StringVar ( & s . ServiceAccounts . JWKSURI , "service-account-jwks-uri" , s . ServiceAccounts . JWKSURI , "" +
"Overrides the URI for the JSON Web Key Set in the discovery doc served at " +
"/.well-known/openid-configuration. This flag is useful if the discovery doc" +
"and key set are served to relying parties from a URL other than the " +
"API server's external (as auto-detected or overridden with external-hostname). " +
"Only valid if the ServiceAccountIssuerDiscovery feature gate is enabled." )
// Deprecated in 1.13
fs . StringSliceVar ( & s . APIAudiences , "service-account-api-audiences" , s . APIAudiences , "" +
"Identifiers of the API. The service account token authenticator will validate that " +
"tokens used against the API are bound to at least one of these audiences." )
fs . MarkDeprecated ( "service-account-api-audiences" , "Use --api-audiences" )
fs . DurationVar ( & s . ServiceAccounts . MaxExpiration , "service-account-max-token-expiration" , s . ServiceAccounts . MaxExpiration , "" +
"The maximum validity duration of a token created by the service account token issuer. If an otherwise valid " +
"TokenRequest with a validity duration larger than this value is requested, a token will be issued with a validity duration of this value." )
fs . BoolVar ( & s . ServiceAccounts . ExtendExpiration , "service-account-extend-token-expiration" , s . ServiceAccounts . ExtendExpiration , "" +
"Turns on projected service account expiration extension during token generation, " +
"which helps safe transition from legacy token to bound service account token feature. " +
"If this flag is enabled, admission injected tokens would be extended up to 1 year to " +
"prevent unexpected failure during transition, ignoring value of service-account-max-token-expiration." )
}
if s . TokenFile != nil {
fs . StringVar ( & s . TokenFile . TokenFile , "token-auth-file" , s . TokenFile . TokenFile , "" +
"If set, the file that will be used to secure the secure port of the API server " +
"via token authentication." )
}
if s . WebHook != nil {
fs . StringVar ( & s . WebHook . ConfigFile , "authentication-token-webhook-config-file" , s . WebHook . ConfigFile , "" +
"File with webhook configuration for token authentication in kubeconfig format. " +
"The API server will query the remote service to determine authentication for bearer tokens." )
fs . StringVar ( & s . WebHook . Version , "authentication-token-webhook-version" , s . WebHook . Version , "" +
"The API version of the authentication.k8s.io TokenReview to send to and expect from the webhook." )
fs . DurationVar ( & s . WebHook . CacheTTL , "authentication-token-webhook-cache-ttl" , s . WebHook . CacheTTL ,
"The duration to cache responses from the webhook token authenticator." )
}
}
func ( s * BuiltInAuthenticationOptions ) ToAuthenticationConfig ( ) ( kubeauthenticator . Config , error ) {
ret := kubeauthenticator . Config {
TokenSuccessCacheTTL : s . TokenSuccessCacheTTL ,
TokenFailureCacheTTL : s . TokenFailureCacheTTL ,
}
if s . Anonymous != nil {
ret . Anonymous = s . Anonymous . Allow
}
if s . BootstrapToken != nil {
ret . BootstrapToken = s . BootstrapToken . Enable
}
if s . ClientCert != nil {
var err error
ret . ClientCAContentProvider , err = s . ClientCert . GetClientCAContentProvider ( )
if err != nil {
return kubeauthenticator . Config { } , err
}
}
if s . OIDC != nil {
ret . OIDCCAFile = s . OIDC . CAFile
ret . OIDCClientID = s . OIDC . ClientID
ret . OIDCGroupsClaim = s . OIDC . GroupsClaim
ret . OIDCGroupsPrefix = s . OIDC . GroupsPrefix
ret . OIDCIssuerURL = s . OIDC . IssuerURL
ret . OIDCUsernameClaim = s . OIDC . UsernameClaim
ret . OIDCUsernamePrefix = s . OIDC . UsernamePrefix
ret . OIDCSigningAlgs = s . OIDC . SigningAlgs
ret . OIDCRequiredClaims = s . OIDC . RequiredClaims
}
if s . RequestHeader != nil {
var err error
ret . RequestHeaderConfig , err = s . RequestHeader . ToAuthenticationRequestHeaderConfig ( )
if err != nil {
return kubeauthenticator . Config { } , err
}
}
ret . APIAudiences = s . APIAudiences
if s . ServiceAccounts != nil {
if s . ServiceAccounts . Issuer != "" && len ( s . APIAudiences ) == 0 {
ret . APIAudiences = authenticator . Audiences { s . ServiceAccounts . Issuer }
}
ret . ServiceAccountKeyFiles = s . ServiceAccounts . KeyFiles
ret . ServiceAccountIssuer = s . ServiceAccounts . Issuer
ret . ServiceAccountLookup = s . ServiceAccounts . Lookup
}
if s . TokenFile != nil {
ret . TokenAuthFile = s . TokenFile . TokenFile
}
if s . WebHook != nil {
ret . WebhookTokenAuthnConfigFile = s . WebHook . ConfigFile
ret . WebhookTokenAuthnVersion = s . WebHook . Version
ret . WebhookTokenAuthnCacheTTL = s . WebHook . CacheTTL
if len ( s . WebHook . ConfigFile ) > 0 && s . WebHook . CacheTTL > 0 {
if s . TokenSuccessCacheTTL > 0 && s . WebHook . CacheTTL < s . TokenSuccessCacheTTL {
klog . Warningf ( "the webhook cache ttl of %s is shorter than the overall cache ttl of %s for successful token authentication attempts." , s . WebHook . CacheTTL , s . TokenSuccessCacheTTL )
}
if s . TokenFailureCacheTTL > 0 && s . WebHook . CacheTTL < s . TokenFailureCacheTTL {
klog . Warningf ( "the webhook cache ttl of %s is shorter than the overall cache ttl of %s for failed token authentication attempts." , s . WebHook . CacheTTL , s . TokenFailureCacheTTL )
}
}
}
return ret , nil
}
// ApplyTo requires already applied OpenAPIConfig and EgressSelector if present.
func ( o * BuiltInAuthenticationOptions ) ApplyTo ( authInfo * genericapiserver . AuthenticationInfo , secureServing * genericapiserver . SecureServingInfo , egressSelector * egressselector . EgressSelector , openAPIConfig * openapicommon . Config , extclient kubernetes . Interface , versionedInformer informers . SharedInformerFactory ) error {
if o == nil {
return nil
}
if openAPIConfig == nil {
return errors . New ( "uninitialized OpenAPIConfig" )
}
authenticatorConfig , err := o . ToAuthenticationConfig ( )
if err != nil {
return err
}
if authenticatorConfig . ClientCAContentProvider != nil {
if err = authInfo . ApplyClientCert ( authenticatorConfig . ClientCAContentProvider , secureServing ) ; err != nil {
return fmt . Errorf ( "unable to load client CA file: %v" , err )
}
}
if authenticatorConfig . RequestHeaderConfig != nil && authenticatorConfig . RequestHeaderConfig . CAContentProvider != nil {
if err = authInfo . ApplyClientCert ( authenticatorConfig . RequestHeaderConfig . CAContentProvider , secureServing ) ; err != nil {
return fmt . Errorf ( "unable to load client CA file: %v" , err )
}
}
authInfo . APIAudiences = o . APIAudiences
if o . ServiceAccounts != nil && o . ServiceAccounts . Issuer != "" && len ( o . APIAudiences ) == 0 {
authInfo . APIAudiences = authenticator . Audiences { o . ServiceAccounts . Issuer }
}
if o . ServiceAccounts . Lookup || utilfeature . DefaultFeatureGate . Enabled ( features . TokenRequest ) {
authenticatorConfig . ServiceAccountTokenGetter = serviceaccountcontroller . NewGetterFromClient (
extclient ,
versionedInformer . Core ( ) . V1 ( ) . Secrets ( ) . Lister ( ) ,
versionedInformer . Core ( ) . V1 ( ) . ServiceAccounts ( ) . Lister ( ) ,
versionedInformer . Core ( ) . V1 ( ) . Pods ( ) . Lister ( ) ,
)
}
authenticatorConfig . BootstrapTokenAuthenticator = bootstrap . NewTokenAuthenticator (
versionedInformer . Core ( ) . V1 ( ) . Secrets ( ) . Lister ( ) . Secrets ( metav1 . NamespaceSystem ) ,
)
if egressSelector != nil {
egressDialer , err := egressSelector . Lookup ( egressselector . Master . AsNetworkContext ( ) )
if err != nil {
return err
}
authenticatorConfig . CustomDial = egressDialer
}
authInfo . Authenticator , openAPIConfig . SecurityDefinitions , err = authenticatorConfig . New ( )
if err != nil {
return err
}
return nil
}
// ApplyAuthorization will conditionally modify the authentication options based on the authorization options
func ( o * BuiltInAuthenticationOptions ) ApplyAuthorization ( authorization * BuiltInAuthorizationOptions ) {
if o == nil || authorization == nil || o . Anonymous == nil {
return
}
// authorization ModeAlwaysAllow cannot be combined with AnonymousAuth.
// in such a case the AnonymousAuth is stomped to false and you get a message
if o . Anonymous . Allow && sets . NewString ( authorization . Modes ... ) . Has ( authzmodes . ModeAlwaysAllow ) {
klog . Warningf ( "AnonymousAuth is not allowed with the AlwaysAllow authorizer. Resetting AnonymousAuth to false. You should use a different authorizer" )
o . Anonymous . Allow = false
}
}