diff --git a/pkg/controller/sessionsecret/session_secret_controller.go b/pkg/controller/sessionsecret/session_secret_controller.go new file mode 100644 index 0000000000..af8dc6d996 --- /dev/null +++ b/pkg/controller/sessionsecret/session_secret_controller.go @@ -0,0 +1,200 @@ +package sessionsecret + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/golang/glog" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + informers "k8s.io/client-go/informers/core/v1" + kcoreclient "k8s.io/client-go/kubernetes/typed/core/v1" + listers "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + cryptohelpers "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/crypto" + "github.com/openshift/library-go/pkg/operator/events" +) + +const ( + sessionSecretNamespace = "openshift-kube-apiserver" + sessionSecretName = "session-secret" +) + +// SessionSecrets struct is copied from github.com/openshift/api/legacyconfig/v1 so we can manually encode and not rely +// on that package. +type SessionSecrets struct { + metav1.TypeMeta `json:",inline"` + + // Secrets is a list of secrets + // New sessions are signed and encrypted using the first secret. + // Existing sessions are decrypted/authenticated by each secret until one succeeds. This allows rotating secrets. + Secrets []SessionSecret `json:"secrets"` +} + +// SessionSecret is a secret used to authenticate/decrypt cookie-based sessions +type SessionSecret struct { + // Authentication is used to authenticate sessions using HMAC. Recommended to use a secret with 32 or 64 bytes. + Authentication string `json:"authentication"` + // Encryption is used to encrypt sessions. Must be 16, 24, or 32 characters long, to select AES-128, AES- + Encryption string `json:"encryption"` +} + +type SessionSecretController struct { + podLister listers.PodLister + secretLister listers.SecretLister + secretClient kcoreclient.SecretsGetter + + secretsHasSynced cache.InformerSynced + podsHasSynced cache.InformerSynced + + syncHandler func(serviceKey string) error + + secretsQueue workqueue.RateLimitingInterface + eventRecorder events.Recorder +} + +func NewSessionSecretController(secrets informers.SecretInformer, pods informers.PodInformer, secretsClient kcoreclient.SecretsGetter, resyncInterval time.Duration, eventRecorder events.Recorder) *SessionSecretController { + sc := &SessionSecretController{ + secretsQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), + } + + sc.podLister = pods.Lister() + sc.secretLister = secrets.Lister() + + pods.Informer().AddEventHandlerWithResyncPeriod( + cache.FilteringResourceEventHandler{ + FilterFunc: isKubeAPIServerPod, + Handler: cache.ResourceEventHandlerFuncs{ + AddFunc: sc.enqueueSecret, + }, + }, + resyncInterval, + ) + + sc.secretClient = secretsClient + sc.secretsHasSynced = secrets.Informer().HasSynced + sc.podsHasSynced = pods.Informer().HasSynced + + sc.syncHandler = sc.syncSecret + sc.eventRecorder = eventRecorder + + return sc +} + +// Run begins watching and syncing. +func (sc *SessionSecretController) Run(workers int, stopCh <-chan struct{}) { + defer utilruntime.HandleCrash() + defer sc.secretsQueue.ShutDown() + + // Wait for the stores to fill + if !cache.WaitForCacheSync(stopCh, sc.secretsHasSynced, sc.podsHasSynced) { + return + } + + glog.V(4).Infof("Starting workers for SessionSecretController") + for i := 0; i < workers; i++ { + go wait.Until(sc.runWorker, time.Second, stopCh) + } + <-stopCh + glog.V(4).Infof("Shutting down SessionSecretController") +} + +// processNextWorkItem deals with one key off the secretsQueue. It returns false when it's time to quit. +func (sc *SessionSecretController) processNextWorkItem() bool { + key, quit := sc.secretsQueue.Get() + if quit { + return false + } + defer sc.secretsQueue.Done(key) + + err := sc.syncHandler(key.(string)) + if err != nil { + utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err)) + sc.eventRecorder.Warningf("CreateSessionSecretFailure", "%v failed with : %v", key, err) + sc.secretsQueue.AddRateLimited(key) + return true + } + + sc.secretsQueue.Forget(key) + return true +} + +func (sc *SessionSecretController) runWorker() { + for sc.processNextWorkItem() { + } +} + +func newSessionSecretsJSON() ([]byte, error) { + secrets := &SessionSecrets{ + TypeMeta: metav1.TypeMeta{ + Kind: "SessionSecrets", + APIVersion: "v1", + }, + Secrets: []SessionSecret{ + { + Authentication: string(cryptohelpers.RandomAuthKeyBits()), + Encryption: string(cryptohelpers.RandomEncKeyBits()), + }, + }, + } + return json.Marshal(secrets) +} + +func (sc *SessionSecretController) createSessionSecret() error { + secretsBytes, err := newSessionSecretsJSON() + if err != nil { + return err + } + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: sessionSecretName, + Namespace: sessionSecretNamespace, + }, + Data: map[string][]byte{ + "secrets": secretsBytes, + }, + } + _, err = sc.secretClient.Secrets(secret.Namespace).Create(secret) + return err +} + +// syncSecret creates the session secret if it doesn't exist. +func (sc *SessionSecretController) syncSecret(key string) error { + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + + _, err = sc.secretLister.Secrets(namespace).Get(name) + if errors.IsNotFound(err) { + glog.V(4).Infof("creating secret %s/%s", namespace, name) + return sc.createSessionSecret() + } + if err != nil { + return err + } + return nil +} + +func (sc *SessionSecretController) enqueueSecret(obj interface{}) { + sc.secretsQueue.Add(sessionSecretNamespace + "/" + sessionSecretName) +} + +func isKubeAPIServerPod(obj interface{}) bool { + pod, ok := obj.(*v1.Pod) + if !ok { + return false + } + if strings.HasPrefix(pod.Name, "openshift-kube-apiserver") { + return true + } + return false +} diff --git a/pkg/operator/configobservation/authconfig/observe_sessionsecret.go b/pkg/operator/configobservation/authconfig/observe_sessionsecret.go new file mode 100644 index 0000000000..6c934fd722 --- /dev/null +++ b/pkg/operator/configobservation/authconfig/observe_sessionsecret.go @@ -0,0 +1,52 @@ +package authconfig + +import ( + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation" + "github.com/openshift/library-go/pkg/operator/configobserver" + "github.com/openshift/library-go/pkg/operator/events" +) + +const ( + sessionSecretNamespace = "openshift-kube-apiserver" + sessionSecretName = "session-secret" + sessionSecretPath = "/etc/kubernetes/static-pod-resources/secrets/session-secret/secret" +) + +// ObserveSessionSecret sets the oauthConfig sessionSecretsFile if it has not been set and the session secret exists +func ObserveSessionSecret(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (map[string]interface{}, []error) { + listers := genericListers.(configobservation.Listers) + errs := []error{} + observedConfig := map[string]interface{}{} + oauthConfigSessionSecretsFilePath := []string{"oauthConfig", "sessionConfig", "sessionSecretsFile"} + + currentSessionSecretsFilePath, _, err := unstructured.NestedString(existingConfig, oauthConfigSessionSecretsFilePath...) + if err != nil { + errs = append(errs, err) + } + if currentSessionSecretsFilePath == sessionSecretPath { + return observedConfig, errs + } + + _, err = listers.SecretLister.Secrets(sessionSecretNamespace).Get(sessionSecretName) + if errors.IsNotFound(err) { + glog.Warningf("session secret %s/%s not found", sessionSecretNamespace, sessionSecretName) + return observedConfig, errs + } + if err != nil { + errs = append(errs, err) + return nil, errs + } + + err = unstructured.SetNestedField(observedConfig, sessionSecretPath, oauthConfigSessionSecretsFilePath...) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + return observedConfig, errs +} diff --git a/pkg/operator/configobservation/authconfig/observe_sessionsecret_test.go b/pkg/operator/configobservation/authconfig/observe_sessionsecret_test.go new file mode 100644 index 0000000000..71398d87a3 --- /dev/null +++ b/pkg/operator/configobservation/authconfig/observe_sessionsecret_test.go @@ -0,0 +1,39 @@ +package authconfig + +import ( + "testing" + + "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + v12 "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" +) + +func TestObserveSessionSecret(t *testing.T) { + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: sessionSecretName, + Namespace: sessionSecretNamespace, + }, + } + indexer.Add(secret) + + listers := configobservation.Listers{ + SecretLister: v12.NewSecretLister(indexer), + } + result, errs := ObserveSessionSecret(listers, nil, map[string]interface{}{}) + if len(errs) > 0 { + t.Error("expected len(errs) == 0") + } + secretPath, _, err := unstructured.NestedString(result, "oauthConfig", "sessionConfig", "sessionSecretsFile") + if err != nil { + t.Fatal(err) + } + if secretPath != sessionSecretPath { + t.Errorf("expected oauthConfig.sessionConfig.sessionSecretsFile: %s, got %s", sessionSecretPath, secretPath) + } +} diff --git a/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go b/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go index 81557f2d5a..d747610a8d 100644 --- a/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go +++ b/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go @@ -10,6 +10,7 @@ import ( kubeapiserveroperatorinformers "github.com/openshift/cluster-kube-apiserver-operator/pkg/generated/informers/externalversions" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation" + "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/authconfig" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/etcd" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/images" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/network" @@ -23,6 +24,7 @@ func NewConfigObserver( operatorClient configobserver.OperatorClient, operatorConfigInformers kubeapiserveroperatorinformers.SharedInformerFactory, kubeInformersForKubeSystemNamespace kubeinformers.SharedInformerFactory, + kubeInformersForOpenshiftKubeAPIServerNamespace kubeinformers.SharedInformerFactory, configInformer configinformers.SharedInformerFactory, eventRecorder events.Recorder, ) *ConfigObserver { @@ -34,6 +36,7 @@ func NewConfigObserver( ImageConfigLister: configInformer.Config().V1().Images().Lister(), EndpointsLister: kubeInformersForKubeSystemNamespace.Core().V1().Endpoints().Lister(), ConfigmapLister: kubeInformersForKubeSystemNamespace.Core().V1().ConfigMaps().Lister(), + SecretLister: kubeInformersForOpenshiftKubeAPIServerNamespace.Core().V1().Secrets().Lister(), ImageConfigSynced: configInformer.Config().V1().Images().Informer().HasSynced, PreRunCachesSynced: []cache.InformerSynced{ operatorConfigInformers.Kubeapiserver().V1alpha1().KubeAPIServerOperatorConfigs().Informer().HasSynced, @@ -44,6 +47,7 @@ func NewConfigObserver( etcd.ObserveStorageURLs, network.ObserveRestrictedCIDRs, images.ObserveInternalRegistryHostname, + authconfig.ObserveSessionSecret, ), } diff --git a/pkg/operator/configobservation/interfaces.go b/pkg/operator/configobservation/interfaces.go index 9785566031..f212ae76b7 100644 --- a/pkg/operator/configobservation/interfaces.go +++ b/pkg/operator/configobservation/interfaces.go @@ -11,6 +11,7 @@ type Listers struct { ImageConfigLister configlistersv1.ImageLister EndpointsLister corelistersv1.EndpointsLister ConfigmapLister corelistersv1.ConfigMapLister + SecretLister corelistersv1.SecretLister ImageConfigSynced cache.InformerSynced diff --git a/pkg/operator/crypto/keybits.go b/pkg/operator/crypto/keybits.go new file mode 100644 index 0000000000..87bafa0b49 --- /dev/null +++ b/pkg/operator/crypto/keybits.go @@ -0,0 +1,35 @@ +package crypto + +// Taken from origin but could be moved to library-go + +import ( + "crypto/rand" + "crypto/sha256" +) + +const ( + sha256KeyLenBits = sha256.BlockSize * 8 // max key size with HMAC SHA256 + aes256KeyLenBits = 256 // max key size with AES (AES-256) +) + +func RandomAuthKeyBits() []byte { + return randomBits(sha256KeyLenBits) +} + +func RandomEncKeyBits() []byte { + return randomBits(aes256KeyLenBits) +} + +// randomBits returns a random byte slice with at least the requested bits of entropy. +// Callers should avoid using a value less than 256 unless they have a very good reason. +func randomBits(bits int) []byte { + size := bits / 8 + if bits%8 != 0 { + size++ + } + b := make([]byte, size) + if _, err := rand.Read(b); err != nil { + panic(err) // rand should never fail + } + return b +} diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index ae23d3c675..1268b4858b 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -8,6 +8,7 @@ import ( configv1client "github.com/openshift/client-go/config/clientset/versioned" configinformers "github.com/openshift/client-go/config/informers/externalversions" "github.com/openshift/cluster-kube-apiserver-operator/pkg/apis/kubeapiserver/v1alpha1" + "github.com/openshift/cluster-kube-apiserver-operator/pkg/controller/sessionsecret" operatorconfigclient "github.com/openshift/cluster-kube-apiserver-operator/pkg/generated/clientset/versioned" operatorclientinformers "github.com/openshift/cluster-kube-apiserver-operator/pkg/generated/informers/externalversions" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/configobservercontroller" @@ -55,10 +56,19 @@ func RunOperator(ctx *controllercmd.ControllerContext) error { schema.GroupVersionResource{Group: v1alpha1.GroupName, Version: "v1alpha1", Resource: "kubeapiserveroperatorconfigs"}, ) + sessionSecretController := sessionsecret.NewSessionSecretController( + kubeInformersForOpenshiftKubeAPIServerNamespace.Core().V1().Secrets(), + kubeInformersForOpenshiftKubeAPIServerNamespace.Core().V1().Pods(), + kubeClient.CoreV1(), + time.Minute, + ctx.EventRecorder, + ) + configObserver := configobservercontroller.NewConfigObserver( staticPodOperatorClient, operatorConfigInformers, kubeInformersForKubeSystemNamespace, + kubeInformersForOpenshiftKubeAPIServerNamespace, configInformers, ctx.EventRecorder, ) @@ -96,6 +106,7 @@ func RunOperator(ctx *controllercmd.ControllerContext) error { kubeInformersForKubeSystemNamespace.Start(ctx.StopCh) configInformers.Start(ctx.StopCh) + go sessionSecretController.Run(1, ctx.StopCh) go staticPodControllers.Run(ctx.StopCh) go targetConfigReconciler.Run(1, ctx.StopCh) go configObserver.Run(1, ctx.StopCh) @@ -123,4 +134,5 @@ var deploymentSecrets = []string{ "etcd-client", "kubelet-client", "serving-cert", + "session-secret", }