Skip to content

Commit

Permalink
Merge pull request #19 from hensur/add-ssh-secrets
Browse files Browse the repository at this point in the history
add ssh secrets
  • Loading branch information
elenz97 authored Apr 8, 2020
2 parents 7ba1fa2 + 2c3a28f commit 5e136e3
Show file tree
Hide file tree
Showing 10 changed files with 1,061 additions and 459 deletions.
73 changes: 66 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,90 @@ $ make uninstall

## Usage

Add the annotation `secret-generator.v1.mittwald.de/autogenerate` to any Kubernetes
secret object. The value of the annotation can be a field name
(or comma separated list of field names) within the secret; the
SecretGeneratorController will pick up this annotation and add a field [or fields]
This operator is capable of generating secure random strings and ssh keypair secrets.

The type of secret to be generated can be specified by the `secret-generator.v1.mittwald.de/type` annotation.
This annotation can be added to any Kubernetes secret object in the operators `watchNamespace`.

### Secure Random Strings

By default, the operator will generate secure random strings. If the type annotation is not present, it will be added after the first
reconciliation loop and its value will be set to `string`.

To actually generate random string secrets, the `secret-generator.v1.mittwald.de/autogenerate` annotation is required as well.
The value of the annotation can be a field name (or comma separated list of field names) within the secret;
the SecretGeneratorController will pick up this annotation and add a field [or fields]
(`password` in the example below) to the secret with a randomly generated string value.

```yaml
apiVersion: v1
kind: Secret
metadata:
name: string-secret
annotations:
secret-generator.v1.mittwald.de/autogenerate: password
data:
username: c29tZXVzZXI=
```
## Operational tasks
after reconciliation:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: string-secret
annotations:
secret-generator.v1.mittwald.de/type: string
secret-generator.v1.mittwald.de/secure: "yes"
secret-generator.v1.mittwald.de/autogenerate: password
secret-generator.v1.mittwald.de/autogenerate-generated-at: "2020-04-03T14:07:47+02:00"
type: Opaque
data:
username: c29tZXVzZXI=
password: TWVwSU83L2huNXBralNTMHFwU3VKSkkwNmN4NmRpNTBBcVpuVDlLOQ==
```
### SSH Key Pairs
To generate SSH Key Pairs, the `secret-generator.v1.mittwald.de/type` annotation **has** to be present on the kubernetes secret object.

The operator will then add two keys to the secret object, `ssh-publickey` and `ssh-privatekey`, each containing the respective key.

- Regenerate all automatically generated passwords:
The Private Key will be PEM encoded, the Public Key will have the authorized-keys format.

```yaml
apiVersion: v1
kind: Secret
metadata:
annotations:
secret-generator.v1.mittwald.de/type: ssh-keypair
data: {}
```

after reconciliation:

```yaml
apiVersion: v1
kind: Secret
metadata:
annotations:
secret-generator.v1.mittwald.de/type: ssh-keypair
secret-generator.v1.mittwald.de/autogenerate-generated-at: "2020-04-03T14:07:47+02:00"
type: Opaque
data:
ssh-publickey: c3NoLXJzYSBBQUFBQ...
ssh-privatekey: LS0tLS1CRUdJTi...
```

## Operational tasks

- Regenerate all automatically generated secrets:
```
$ kubectl annotate secrets --all secret-generator.v1.mittwald.de/regenerate=true
```

- Regenerate only certain fields
- Regenerate only certain fields, in case the secret is of the `password` type:
```
$ kubectl annotate secrets --all secret-generator.v1.mittwald.de/regenerate=password1,password2
```
5 changes: 5 additions & 0 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func main() {

pflag.Bool("regenerate-insecure", false, "Set this to automatically regenerate secrets that were generated with an non-cryptographically secure PRNG.")
pflag.Int("secret-length", 40, "Secret length")
pflag.Int("ssh-key-length", 2048, "Default length of SSH Keys")

pflag.Parse()

Expand All @@ -83,6 +84,10 @@ func main() {
panic(fmt.Errorf("parameter secret-length is set to 0"))
}

if viper.GetInt("ssh-key-length") == 0 {
panic(fmt.Errorf("parameter ssh-key-length is set to 0"))
}

// Use a zap logr.Logger implementation. If none of the zap
// flags are configured (or if the zap flag set is not being
// used), this defaults to a production zap logger.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ module github.com/mittwald/kubernetes-secret-generator
go 1.13

require (
github.com/go-logr/logr v0.1.0
github.com/google/uuid v1.1.1
github.com/imdario/mergo v0.3.8
github.com/operator-framework/operator-sdk v0.16.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152
k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0
k8s.io/client-go v12.0.0+incompatible
Expand Down
121 changes: 36 additions & 85 deletions pkg/controller/secret/secret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package secret

import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/spf13/viper"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -17,19 +14,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"strings"
"strconv"
"time"
)

var log = logf.Log.WithName("controller_secret")

const (
SecretGenerateAnnotation = "secret-generator.v1.mittwald.de/autogenerate"
SecretGeneratedAtAnnotation = "secret-generator.v1.mittwald.de/autogenerate-generated-at"
SecretRegenerateAnnotation = "secret-generator.v1.mittwald.de/regenerate"
SecretSecureAnnotation = "secret-generator.v1.mittwald.de/secure"
)

func regenerateInsecure() bool {
return viper.GetBool("regenerate-insecure")
}
Expand All @@ -38,6 +28,10 @@ func secretLength() int {
return viper.GetInt("secret-length")
}

func sshKeyLength() int {
return viper.GetInt("ssh-key-length")
}

// Add creates a new Secret Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func Add(mgr manager.Manager) error {
Expand Down Expand Up @@ -102,71 +96,47 @@ func (r *ReconcileSecret) Reconcile(request reconcile.Request) (reconcile.Result

desired := instance.DeepCopy()

toGenerate, ok := desired.Annotations[SecretGenerateAnnotation]
if !ok {
return reconcile.Result{}, nil
}

reqLogger.Info("instance is autogenerated")

genKeys := strings.Split(toGenerate, ",")
sType := SecretType(desired.Annotations[AnnotationSecretType])
if err := sType.Validate(); err != nil {
if _, ok := desired.Annotations[AnnotationSecretAutoGenerate]; !ok && sType == "" {
// return if secret has no type and no autogenerate annotation
return reconcile.Result{}, nil
}

if err := ensureUniqueness(genKeys); err != nil {
return reconcile.Result{}, err
// keep backwards compatibility by defaulting to string type
desired.Annotations[AnnotationSecretType] = string(SecretTypeString)
sType = SecretTypeString
}

var regenKeys []string
if _, ok := desired.Annotations[SecretSecureAnnotation]; !ok && regenerateInsecure() {
reqLogger.Info("instance was generated by cryptographically insecure PNRG")
regenKeys = genKeys // regenerate all keys
} else {
if regenerate, ok := desired.Annotations[SecretRegenerateAnnotation]; ok {
reqLogger.Info("removing regenerate annotation from instance")
delete(desired.Annotations, SecretRegenerateAnnotation)

if regenerate == "yes" {
regenKeys = genKeys
} else {
regenKeys = strings.Split(regenerate, ",") // regenerate requested keys
}
}
}
reqLogger = reqLogger.WithValues("type", sType)
reqLogger.Info("instance is autogenerated")

if desired.Data == nil {
desired.Data = make(map[string][]byte)
}

generatedCount := 0
for _, key := range genKeys {
if len(desired.Data[key]) != 0 && !contains(regenKeys, key) {
// dont generate key if it already has a value
// and is not queued for regeneration
continue
var generator SecretGenerator
switch sType {
case SecretTypeSSHKeypair:
generator = SSHKeypairGenerator{
log: reqLogger.WithValues("type", SecretTypeSSHKeypair),
}
generatedCount++

value, err := generateSecret(secretLength())
if err != nil {
reqLogger.Error(err, "could not generate new instance")
return reconcile.Result{RequeueAfter: time.Second * 30}, err
case SecretTypeString:
generator = StringGenerator{
log: reqLogger.WithValues("type", SecretTypeString),
}

desired.Data[key] = []byte(value)

reqLogger.Info("set field of instance to new randomly generated instance", "bytes", len(value), "field", key)
}
reqLogger.Info("generated secrets", "count", generatedCount)

if generatedCount == len(genKeys) {
// all keys have been generated by this instance
desired.Annotations[SecretSecureAnnotation] = "yes"
res, err := generator.generateData(desired)
if err != nil {
return res, err
}

if !reflect.DeepEqual(instance.Annotations, desired.Annotations) ||
!reflect.DeepEqual(instance.Data, desired.Data) {
reqLogger.Info("updating secret")

desired.Annotations[SecretGeneratedAtAnnotation] = time.Now().String()
desired.Annotations[AnnotationSecretAutoGeneratedAt] = time.Now().Format(time.RFC3339)
err := r.client.Update(context.Background(), desired)
if err != nil {
reqLogger.Error(err, "could not update secret")
Expand All @@ -177,33 +147,14 @@ func (r *ReconcileSecret) Reconcile(request reconcile.Request) (reconcile.Result
return reconcile.Result{}, nil
}

func generateSecret(length int) (string, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return "", err
}

return base64.StdEncoding.EncodeToString(b)[0:length], nil
}

func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

// ensure elements in input array are unique
func ensureUniqueness(a []string) error {
set := map[string]bool{}
for _, e := range a {
if set[e] {
return fmt.Errorf("duplicate element %s found", e)
func secretLengthFromAnnotation(fallback int, annotations map[string]string) (int, error) {
l := fallback
if val, ok := annotations[AnnotationSecretLength]; ok {
intVal, err := strconv.Atoi(val)
if err != nil {
return 0, err
}
set[e] = true
l = intVal
}
return nil
return l, nil
}
Loading

0 comments on commit 5e136e3

Please sign in to comment.