-
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 5, 2018
1 parent
a7a65c9
commit ec158e6
Showing
7 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
200 changes: 200 additions & 0 deletions
200
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,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 | ||
} |
52 changes: 52 additions & 0 deletions
52
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,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 | ||
} |
39 changes: 39 additions & 0 deletions
39
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,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) | ||
} | ||
} |
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
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
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,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 | ||
} |
Oops, something went wrong.