-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Matt Rogers
committed
Dec 7, 2018
1 parent
a7a65c9
commit d01b3bf
Showing
6 changed files
with
382 additions
and
0 deletions.
There are no files selected for viewing
227 changes: 227 additions & 0 deletions
227
pkg/controller/sessionsecret/session_secret_controller.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
package sessionsecret | ||
|
||
import ( | ||
"crypto/rand" | ||
"crypto/sha256" | ||
"encoding/json" | ||
"fmt" | ||
"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 { | ||
secretLister listers.SecretLister | ||
secretClient kcoreclient.SecretsGetter | ||
|
||
secretsHasSynced cache.InformerSynced | ||
syncHandler func(serviceKey string) error | ||
|
||
secretsQueue workqueue.RateLimitingInterface | ||
eventRecorder events.Recorder | ||
} | ||
|
||
func NewSessionSecretController(secrets informers.SecretInformer, secretsClient kcoreclient.SecretsGetter, resyncInterval time.Duration, eventRecorder events.Recorder) *SessionSecretController { | ||
sc := &SessionSecretController{ | ||
secretsQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()), | ||
} | ||
|
||
sc.secretLister = secrets.Lister() | ||
|
||
secrets.Informer().AddEventHandlerWithResyncPeriod( | ||
cache.FilteringResourceEventHandler{ | ||
FilterFunc: isSessionSecret, | ||
Handler: cache.ResourceEventHandlerFuncs{ | ||
AddFunc: sc.enqueueSecret, | ||
}, | ||
}, | ||
resyncInterval, | ||
) | ||
|
||
sc.secretClient = secretsClient | ||
sc.secretsHasSynced = secrets.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) { | ||
return | ||
} | ||
|
||
glog.V(4).Infof("Starting workers for SessionSecretController") | ||
for i := 0; i < workers; i++ { | ||
go wait.Until(sc.runWorker, time.Second, stopCh) | ||
} | ||
|
||
// Add a queue item to trigger initial creation of the secret | ||
sc.enqueueSecret(nil) | ||
|
||
<-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) | ||
if errors.IsAlreadyExists(err) { | ||
return nil | ||
} | ||
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 isSessionSecret(obj interface{}) bool { | ||
secret, ok := obj.(*v1.Secret) | ||
if !ok { | ||
return false | ||
} | ||
return secret.Namespace == sessionSecretNamespace && secret.Name == sessionSecretName | ||
} |
71 changes: 71 additions & 0 deletions
71
pkg/operator/configobservation/authconfig/observe_sessionsecret.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
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/unsets the oauthConfig sessionSecretsFile depending on if 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{} | ||
prevObservedConfig := map[string]interface{}{} | ||
|
||
oauthConfigSessionSecretsFilePath := []string{"oauthConfig", "sessionConfig", "sessionSecretsFile"} | ||
currentSessionSecretsFilePath, _, err := unstructured.NestedString(existingConfig, oauthConfigSessionSecretsFilePath...) | ||
if err != nil { | ||
errs = append(errs, err) | ||
} | ||
if len(currentSessionSecretsFilePath) > 0 { | ||
if err := unstructured.SetNestedField(prevObservedConfig, currentSessionSecretsFilePath, oauthConfigSessionSecretsFilePath...); err != nil { | ||
errs = append(errs, err) | ||
} | ||
} | ||
|
||
if !listers.SecretHasSynced() { | ||
glog.Warningf("secrets not synced") | ||
return prevObservedConfig, errs | ||
} | ||
|
||
observedConfig := map[string]interface{}{} | ||
_, err = listers.SecretLister.Secrets(sessionSecretNamespace).Get(sessionSecretName) | ||
if errors.IsNotFound(err) { | ||
glog.Warningf("session secret %s/%s not found", sessionSecretNamespace, sessionSecretName) | ||
// Unset the value if we need to. | ||
if len(currentSessionSecretsFilePath) > 0 { | ||
err := unstructured.SetNestedField(observedConfig, "", oauthConfigSessionSecretsFilePath...) | ||
if err != nil { | ||
errs = append(errs, err) | ||
} | ||
} | ||
return observedConfig, errs | ||
} | ||
if err != nil { | ||
return prevObservedConfig, errs | ||
} | ||
|
||
// Secret is found and sessionSecretPath is already set. | ||
if len(currentSessionSecretsFilePath) > 0 { | ||
return prevObservedConfig, errs | ||
} | ||
|
||
// Set sessionSecretPath | ||
err = unstructured.SetNestedField(observedConfig, sessionSecretPath, oauthConfigSessionSecretsFilePath...) | ||
if err != nil { | ||
errs = append(errs, err) | ||
} | ||
|
||
return observedConfig, errs | ||
} |
65 changes: 65 additions & 0 deletions
65
pkg/operator/configobservation/authconfig/observe_sessionsecret_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
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) | ||
} | ||
} | ||
|
||
func TestDeleteSessionSecret(t *testing.T) { | ||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) | ||
|
||
listers := configobservation.Listers{ | ||
SecretLister: v12.NewSecretLister(indexer), | ||
} | ||
existingConfig := map[string]interface{}{} | ||
oauthConfigSessionSecretsFilePath := []string{"oauthConfig", "sessionConfig", "sessionSecretsFile"} | ||
|
||
err := unstructured.SetNestedField(existingConfig, "foobar", oauthConfigSessionSecretsFilePath...) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
result, errs := ObserveSessionSecret(listers, nil, existingConfig) | ||
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 len(secretPath) > 0 { | ||
t.Errorf("expected oauthConfig.sessionConfig.sessionSecretsFile: \"\", got %s", secretPath) | ||
} | ||
} |
Oops, something went wrong.