cd686ffdf3
This change updates the TLS config used by all pinniped components. There are no configuration knobs associated with this change. Thus this change tightens our static defaults. There are four TLS config levels: 1. Secure (TLS 1.3 only) 2. Default (TLS 1.2+ best ciphers that are well supported) 3. Default LDAP (TLS 1.2+ with less good ciphers) 4. Legacy (currently unused, TLS 1.2+ with all non-broken ciphers) Highlights per component: 1. pinniped CLI - uses "secure" config against KAS - uses "default" for all other connections 2. concierge - uses "secure" config as an aggregated API server - uses "default" config as a impersonation proxy API server - uses "secure" config against KAS - uses "default" config for JWT authenticater (mostly, see code) - no changes to webhook authenticater (see code) 3. supervisor - uses "default" config as a server - uses "secure" config against KAS - uses "default" config against OIDC IDPs - uses "default LDAP" config against LDAP IDPs Signed-off-by: Monis Khan <mok@vmware.com>
223 lines
7.7 KiB
Go
223 lines
7.7 KiB
Go
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package jwtcachefiller implements a controller for filling an authncache.Cache with each
|
|
// added/updated JWTAuthenticator.
|
|
package jwtcachefiller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"reflect"
|
|
"time"
|
|
|
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
|
"github.com/go-logr/logr"
|
|
"gopkg.in/square/go-jose.v2"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
|
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
|
|
"k8s.io/klog/v2"
|
|
|
|
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
|
authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1"
|
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
|
pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator"
|
|
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
|
"go.pinniped.dev/internal/controllerlib"
|
|
"go.pinniped.dev/internal/net/phttp"
|
|
)
|
|
|
|
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
|
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
|
const (
|
|
defaultUsernameClaim = "username"
|
|
defaultGroupsClaim = "groups"
|
|
)
|
|
|
|
// defaultSupportedSigningAlgos returns the default signing algos that this JWTAuthenticator
|
|
// supports (i.e., if none are supplied by the user).
|
|
func defaultSupportedSigningAlgos() []string {
|
|
return []string{
|
|
// RS256 is recommended by the OIDC spec and required, in some capacity. Since we want the
|
|
// JWTAuthenticator to be able to support many OIDC ID tokens out of the box, we include this
|
|
// algorithm by default.
|
|
string(jose.RS256),
|
|
// ES256 is what the Supervisor does, by default. We want integration with the JWTAuthenticator
|
|
// to be as seamless as possible, so we include this algorithm by default.
|
|
string(jose.ES256),
|
|
}
|
|
}
|
|
|
|
type tokenAuthenticatorCloser interface {
|
|
authenticator.Token
|
|
pinnipedauthenticator.Closer
|
|
}
|
|
|
|
type jwtAuthenticator struct {
|
|
tokenAuthenticatorCloser
|
|
spec *auth1alpha1.JWTAuthenticatorSpec
|
|
}
|
|
|
|
// New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
|
|
func New(
|
|
cache *authncache.Cache,
|
|
jwtAuthenticators authinformers.JWTAuthenticatorInformer,
|
|
log logr.Logger,
|
|
) controllerlib.Controller {
|
|
return controllerlib.New(
|
|
controllerlib.Config{
|
|
Name: "jwtcachefiller-controller",
|
|
Syncer: &controller{
|
|
cache: cache,
|
|
jwtAuthenticators: jwtAuthenticators,
|
|
log: log.WithName("jwtcachefiller-controller"),
|
|
},
|
|
},
|
|
controllerlib.WithInformer(
|
|
jwtAuthenticators,
|
|
pinnipedcontroller.MatchAnythingFilter(nil), // nil parent func is fine because each event is distinct
|
|
controllerlib.InformerOption{},
|
|
),
|
|
)
|
|
}
|
|
|
|
type controller struct {
|
|
cache *authncache.Cache
|
|
jwtAuthenticators authinformers.JWTAuthenticatorInformer
|
|
log logr.Logger
|
|
}
|
|
|
|
// Sync implements controllerlib.Syncer.
|
|
func (c *controller) Sync(ctx controllerlib.Context) error {
|
|
obj, err := c.jwtAuthenticators.Lister().Get(ctx.Key.Name)
|
|
if err != nil && errors.IsNotFound(err) {
|
|
c.log.Info("Sync() found that the JWTAuthenticator does not exist yet or was deleted")
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get JWTAuthenticator %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err)
|
|
}
|
|
|
|
cacheKey := authncache.Key{
|
|
APIGroup: auth1alpha1.GroupName,
|
|
Kind: "JWTAuthenticator",
|
|
Name: ctx.Key.Name,
|
|
}
|
|
|
|
// If this authenticator already exists, then only recreate it if is different from the desired
|
|
// authenticator. We don't want to be creating a new authenticator for every resync period.
|
|
//
|
|
// If we do need to recreate the authenticator, then make sure we close the old one to avoid
|
|
// goroutine leaks.
|
|
if value := c.cache.Get(cacheKey); value != nil {
|
|
jwtAuthenticator := c.extractValueAsJWTAuthenticator(value)
|
|
if jwtAuthenticator != nil {
|
|
if reflect.DeepEqual(jwtAuthenticator.spec, &obj.Spec) {
|
|
c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer).Info("actual jwt authenticator and desired jwt authenticator are the same")
|
|
return nil
|
|
}
|
|
jwtAuthenticator.Close()
|
|
}
|
|
}
|
|
|
|
// Make a deep copy of the spec so we aren't storing pointers to something that the informer cache
|
|
// may mutate!
|
|
jwtAuthenticator, err := newJWTAuthenticator(obj.Spec.DeepCopy())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build jwt authenticator: %w", err)
|
|
}
|
|
|
|
c.cache.Store(cacheKey, jwtAuthenticator)
|
|
c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer).Info("added new jwt authenticator")
|
|
return nil
|
|
}
|
|
|
|
func (c *controller) extractValueAsJWTAuthenticator(value authncache.Value) *jwtAuthenticator {
|
|
jwtAuthenticator, ok := value.(*jwtAuthenticator)
|
|
if !ok {
|
|
actualType := "<nil>"
|
|
if t := reflect.TypeOf(value); t != nil {
|
|
actualType = t.String()
|
|
}
|
|
c.log.WithValues("actualType", actualType).Info("wrong JWT authenticator type in cache")
|
|
return nil
|
|
}
|
|
return jwtAuthenticator
|
|
}
|
|
|
|
// newJWTAuthenticator creates a jwt authenticator from the provided spec.
|
|
func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthenticator, error) {
|
|
rootCAs, caBundle, err := pinnipedauthenticator.CABundle(spec.TLS)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid TLS configuration: %w", err)
|
|
}
|
|
|
|
var caContentProvider oidc.CAContentProvider
|
|
if len(caBundle) != 0 {
|
|
var caContentProviderErr error
|
|
caContentProvider, caContentProviderErr = dynamiccertificates.NewStaticCAContent("ignored", caBundle)
|
|
if caContentProviderErr != nil {
|
|
return nil, caContentProviderErr // impossible since caBundle is validated already
|
|
}
|
|
}
|
|
usernameClaim := spec.Claims.Username
|
|
if usernameClaim == "" {
|
|
usernameClaim = defaultUsernameClaim
|
|
}
|
|
groupsClaim := spec.Claims.Groups
|
|
if groupsClaim == "" {
|
|
groupsClaim = defaultGroupsClaim
|
|
}
|
|
|
|
// copied from Kube OIDC code
|
|
issuerURL, err := url.Parse(spec.Issuer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if issuerURL.Scheme != "https" {
|
|
return nil, fmt.Errorf("issuer (%q) has invalid scheme (%q), require 'https'", spec.Issuer, issuerURL.Scheme)
|
|
}
|
|
|
|
client := phttp.Default(rootCAs)
|
|
client.Timeout = 30 * time.Second // copied from Kube OIDC code
|
|
|
|
ctx := coreosoidc.ClientContext(context.Background(), client)
|
|
|
|
provider, err := coreosoidc.NewProvider(ctx, spec.Issuer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not initialize provider: %w", err)
|
|
}
|
|
providerJSON := &struct {
|
|
JWKSURL string `json:"jwks_uri"`
|
|
}{}
|
|
if err := provider.Claims(providerJSON); err != nil {
|
|
return nil, fmt.Errorf("could not get provider jwks_uri: %w", err) // should be impossible because coreosoidc.NewProvider validates this
|
|
}
|
|
if len(providerJSON.JWKSURL) == 0 {
|
|
return nil, fmt.Errorf("issuer %q does not have jwks_uri set", spec.Issuer)
|
|
}
|
|
|
|
oidcAuthenticator, err := oidc.New(oidc.Options{
|
|
IssuerURL: spec.Issuer,
|
|
KeySet: coreosoidc.NewRemoteKeySet(ctx, providerJSON.JWKSURL),
|
|
ClientID: spec.Audience,
|
|
UsernameClaim: usernameClaim,
|
|
GroupsClaim: groupsClaim,
|
|
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
|
|
// this is still needed for distributed claim resolution, meaning this uses a http client that does not honor our TLS config
|
|
// TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106141
|
|
CAContentProvider: caContentProvider,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not initialize authenticator: %w", err)
|
|
}
|
|
|
|
return &jwtAuthenticator{
|
|
tokenAuthenticatorCloser: oidcAuthenticator,
|
|
spec: spec,
|
|
}, nil
|
|
}
|