Skip to content

Commit

Permalink
Manage sessionSecretsFile
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Rogers committed Dec 6, 2018
1 parent a7a65c9 commit 7c6439c
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 0 deletions.
229 changes: 229 additions & 0 deletions pkg/controller/sessionsecret/session_secret_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package sessionsecret

import (
"crypto/rand"
"crypto/sha256"
"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"

"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"`
}

// Taken from origin but could be moved to library-go
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
}

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(randomAuthKeyBits()),
Encryption: string(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
}
52 changes: 52 additions & 0 deletions pkg/operator/configobservation/authconfig/observe_sessionsecret.go
Original file line number Diff line number Diff line change
@@ -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 observedConfig, errs
}

err = unstructured.SetNestedField(observedConfig, sessionSecretPath, oauthConfigSessionSecretsFilePath...)
if err != nil {
errs = append(errs, err)
return observedConfig, errs
}

return observedConfig, errs
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -34,16 +36,19 @@ 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,
kubeInformersForKubeSystemNamespace.Core().V1().Endpoints().Informer().HasSynced,
kubeInformersForKubeSystemNamespace.Core().V1().ConfigMaps().Informer().HasSynced,
kubeInformersForKubeSystemNamespace.Core().V1().Secrets().Informer().HasSynced,
},
},
etcd.ObserveStorageURLs,
network.ObserveRestrictedCIDRs,
images.ObserveInternalRegistryHostname,
authconfig.ObserveSessionSecret,
),
}

Expand Down
1 change: 1 addition & 0 deletions pkg/operator/configobservation/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Listers struct {
ImageConfigLister configlistersv1.ImageLister
EndpointsLister corelistersv1.EndpointsLister
ConfigmapLister corelistersv1.ConfigMapLister
SecretLister corelistersv1.SecretLister

ImageConfigSynced cache.InformerSynced

Expand Down
Loading

0 comments on commit 7c6439c

Please sign in to comment.