Skip to content

Commit

Permalink
Implemented new command
Browse files Browse the repository at this point in the history
  • Loading branch information
ryan0x44 committed Mar 15, 2023
1 parent e051d75 commit a01190c
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 84 deletions.
49 changes: 38 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,10 @@ Or you can build/install using standard [Go](https://go.dev/doc/tutorial/compile

## Usage

### Rotate
### Loading values from files

Rotate secrets in an existing SealedSecret:

```
kubesealplus rotate templates/secret-password.production.yaml
```
When using the `new` or `rotate` commands please note the following for values:

You'll be prompted to input secret values for each existing key then confirm
before the file is written to;
* Enter a value and press return (newline) to complete the value
* If you want to skip rotating some keys, don't enter anything for that key and
press return (newline)
* When a string literal is used as the value, white space will be trimmed
including leading and trailing spaces, tabs, and newline characters (per Go's
strings.TrimSpace)
Expand All @@ -86,6 +77,36 @@ before the file is written to;
and the auto-detection will not run (as first char will not be `/`, and the
leading space will be trimmed)

### New command

Create a new SealedSecret from scratch:

```
kubesealplus new templates/secret-password.production.yaml
```

You'll be prompted to:
1. Enter the namespace to scope your SealedSecret to.
2. Enter the key-value pairs per line, separated by an equals sign (`=`).
3. Once done entering key-value pairs, press enter on a blank line.
4. Review your entered key-value pairs and ensure they were parsed correctly
(e.g. per the rules/logic noted above)

### Rotate commnad

Rotate secrets in an existing SealedSecret:

```
kubesealplus rotate templates/secret-password.production.yaml
```

You'll be prompted to input secret values for each existing key then confirm
before the file is written to;
* Enter a value and press return (newline) to complete the value
* If you want to skip rotating some keys, don't enter anything for that key and
press return (newline)
* Take note of the rules/logic noted above

### Config

Configure the Sealed Secret public key/cert URL for the `production`
Expand All @@ -95,6 +116,12 @@ environment:
kubesealplus config production cert https://sealed-secrets-controller.production.example.com/v1/cert.pem
```

Otherwise configure it from a local file:

```
kubesealplus config production cert /path/to/cert.pem
```

## Sharing your Public Cert/Key

Per the `config ... cert ...` usage instructions above, this tool can fetch the
Expand Down
129 changes: 88 additions & 41 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"path/filepath"
"regexp"
"strings"
"time"
)

func nameAndEnvFromFilename(path string) (name string, environment string, err error) {
Expand Down Expand Up @@ -40,6 +39,12 @@ func main() {
command = os.Args[1]
}
switch command {
case "new":
if len(os.Args) != 3 || len(os.Args[2]) == 0 {
fmt.Printf("Usage:\n\tkubesealplus new (sealedsecret-filename.yaml)\n")
os.Exit(1)
}
new(os.Args[2])
case "rotate":
if len(os.Args) != 3 || len(os.Args[2]) == 0 {
fmt.Printf("Usage:\n\tkubesealplus rotate (sealedsecret-filename.yaml)\n")
Expand Down Expand Up @@ -102,72 +107,128 @@ func configure(environment string, configKey string, configValue string) {
fmt.Printf("Cert value '%s'\nfor environment '%s'\nsuccessfully saved to config file '%s'\n", configValue, environment, configFile)
}

func rotate(filename string) {
secretName, environment, err := nameAndEnvFromFilename(filename)
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}

func loadConfig(environment string) (certFilename string, err error) {
configFile, err := ConfigFileDefaultPath("")
if err != nil {
panic(err)
}
configDoc := ConfigDoc{}
if !configDoc.Exists(configFile) {
fmt.Printf("Config for environment '%s' not found. Run this:\n"+
err = fmt.Errorf("Config for environment '%s' not found. Run this:\n"+
"kubesealplus config %s cert (your-cert-file)", environment, environment)
os.Exit(1)
return
}
err = configDoc.Load(configFile)
if err != nil {
fmt.Printf("Error loading config file %s: %s\n", configFile, err)
os.Exit(1)
err = fmt.Errorf("Error loading config file %s: %s\n", configFile, err)
return
}

certConfigValue := configDoc.Environments[environment]["cert"]
// TODO: implement caching of cert load.
// we probably only need to download it at most once per hour (or day?)
cert, err := CertLoad(certConfigValue)
if err != nil {
fmt.Printf("Unable to load cert '%s':\n%s\n", certConfigValue, err)
err = fmt.Errorf("unable to load cert '%s':\n%s\n", certConfigValue, err)
return
}
certFilename, err = ConfigWriteCert(environment, cert)
if err != nil {
err = fmt.Errorf("Unable to write latest cert to disk:\n%s\n", err)
return
}
return
}

func new(filename string) {
fileInfo, err := os.Stat(filename)
if err == nil && fileInfo != nil {
fmt.Printf("Error: cannot create new file as file already exists\n\t%s\n", filename)
os.Exit(1)
}

secretName, environment, err := nameAndEnvFromFilename(filename)
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}
certFilename, err := ConfigWriteCert(environment, cert)
secrets := PromptSecrets{}
namespace, err := secrets.Namespace(os.Stdin, os.Stdout)
if err != nil {
fmt.Printf("Unable to write latest cert to disk:\n%s\n", err)
fmt.Printf("%s\n", err)
os.Exit(1)
}

sealedSecret := SealedSecret{Environment: environment}
sealedSecret.Init(secretName, namespace)

rotateAndNew(&sealedSecret, secrets)

file, err := os.Create(filename)
if err != nil {
fmt.Printf("Error creating new file: %s\n", err)
os.Exit(1)
}
defer file.Close()
out, err := sealedSecret.ToTemplate(file, environment)
if err != nil {
fmt.Printf("error writing SealedSecret file %s: %s\n", filename, err)
os.Exit(1)
}
fmt.Printf("Created SealedSecret file '%s' with content:\n%s", filename, out.String())
}

func rotate(filename string) {
file, err := os.OpenFile(filename, os.O_RDWR, 0644)
if err != nil {
fmt.Printf("Cannot open file: %s\n", filename)
os.Exit(1)
}
defer file.Close()

template, err := io.ReadAll(file)
if err != nil {
fmt.Printf("Cannot read file: %s\n", filename)
os.Exit(1)
}

_, environment, err := nameAndEnvFromFilename(filename)
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}
sealedSecret, err := sealedSecretFromTemplate(filename, environment, string(template))
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}

keys := []string{}
secrets := PromptSecrets{}
for k := range sealedSecret.Spec.EncryptedData {
keys = append(keys, k)
secrets.InitKey(k)
}
rotateAndNew(&sealedSecret, secrets)

out, err := sealedSecret.ToTemplate(file, environment)
if err != nil {
fmt.Printf("error writing SealedSecret file %s: %s\n", filename, err)
os.Exit(1)
}
fmt.Printf("Updated SealedSecret file '%s' with content:\n%s", filename, out.String())
}

func rotateAndNew(sealedSecret *SealedSecret, secrets PromptSecrets) {
certFilename, err := loadConfig(sealedSecret.Environment)
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}

secrets := PromptSecrets{}
secrets.InitKeys(keys)
redo := 0
for {
err = secrets.Enter(redo, os.Stdin, os.Stdout)
var err error
if len(secrets.secrets) > 0 {
err = secrets.Update(redo, os.Stdin, os.Stdout)
} else {
err = secrets.Enter(os.Stdin, os.Stdout)
}
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
Expand All @@ -183,18 +244,7 @@ func rotate(filename string) {
}
PromptClear(os.Stdout)

// TODO: support creating new sealed secrets from scratch
newSecrets := secrets.ToValues()
if len(sealedSecret.Spec.Template.Metadata) == 0 {
timestamp := time.Now().UTC().Format(time.RFC3339)
// TODO: get namespace for new secrets
secretNamespace := ""
sealedSecret.Spec.Template.Metadata = map[string]*string{
"creationTimestamp": &timestamp,
"name": &secretName,
"namespace": &secretNamespace,
}
}
secretYAML, err := createSecretYAML(
sealedSecret.Spec.Template.Metadata,
newSecrets,
Expand All @@ -203,6 +253,7 @@ func rotate(filename string) {
fmt.Printf("error creating Secret:\n%s\n", err)
os.Exit(1)
}

newSealedSecrets, err := createSealedSecrets(secretYAML, certFilename)
if err != nil {
fmt.Printf("error creating SealedSecret via kubeseal:\n%s\n", err)
Expand All @@ -213,14 +264,10 @@ func rotate(filename string) {
"number of secrets returned do not match number given")
os.Exit(1)
}
if sealedSecret.Spec.EncryptedData == nil {
sealedSecret.Spec.EncryptedData = map[string]string{}
}
for k, v := range newSealedSecrets {
sealedSecret.Spec.EncryptedData[k] = v
}
out, err := sealedSecret.ToTemplate(file, environment)
if err != nil {
fmt.Printf("error writing SealedSecret to template %s:\n%s\n", filename, err)
os.Exit(1)
}
fmt.Printf("Wrote new SealedSecret to file\n%s\nwith content:\n%s", filename, out.String())

}
36 changes: 18 additions & 18 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,26 @@ func TestEnvFromFilename(t *testing.T) {
expectError bool
}{
{
filename: "/path/to/templates/secret-example.production.yaml",
filename: "/path/to/templates/secret-example.testing.yaml",
expectName: "example",
expectEnv: "production",
expectEnv: "testing",
},
{
filename: "./templates/secret-example.production.yaml",
filename: "./templates/secret-example.testing.yaml",
expectName: "example",
expectEnv: "production",
expectEnv: "testing",
}, {
filename: "templates/secret-example.production.yaml",
filename: "templates/secret-example.testing.yaml",
expectName: "example",
expectEnv: "production",
expectEnv: "testing",
},
{
filename: "secret-example.production.yaml",
filename: "secret-example.testing.yaml",
expectName: "example",
expectEnv: "production",
expectEnv: "testing",
},
{
filename: "secret-example.test.production.yaml",
filename: "secret-example.test.testing.yaml",
expectError: true,
},
{
Expand All @@ -48,7 +48,7 @@ func TestEnvFromFilename(t *testing.T) {
expectError: true,
},
{
filename: "secret-example.production.yml",
filename: "secret-example.testing.yml",
expectError: true,
},
}
Expand All @@ -75,7 +75,7 @@ func TestSealedSecretFromTemplate(t *testing.T) {
{
expectError: false,
data: `
{{- if eq .Values.environment "production" }}
{{- if eq .Values.environment "testing" }}
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
Expand All @@ -95,9 +95,9 @@ spec:
},
{
expectError: false,
expectSealedSecret: &SealedSecret{ApiVersion: "bitnami.com/v1alpha1"},
expectSealedSecret: &SealedSecret{Environment: "testing", ApiVersion: "bitnami.com/v1alpha1"},
data: `
{{- if eq .Values.environment "production" }}
{{- if eq .Values.environment "testing" }}
apiVersion: "bitnami.com/v1alpha1"
{{- end }}`,
},
Expand All @@ -108,7 +108,7 @@ apiVersion: "bitnami.com/v1alpha1"
{
expectError: true,
data: `
{{- if eq .Values.environment "production" }}
{{- if eq .Values.environment "testing" }}
{{- end }}
`,
},
Expand All @@ -120,17 +120,17 @@ apiVersion: "bitnami.com/v1alpha1"
`,
},
}
filename := "templates/secret-example.production.yaml"
environment := "production"
filename := "templates/secret-example.testing.yaml"
environment := "testing"
i := 0
for _, test := range tests {
i++
sealedSecret, err := sealedSecretFromTemplate(filename, environment, test.data)
if err != nil && !test.expectError {
t.Errorf("Unexpected error in test %d: %s", i, err)
t.Errorf("(Test %d)Unexpected error: %s", i, err)
}
if err == nil && test.expectSealedSecret != nil && !reflect.DeepEqual(*test.expectSealedSecret, sealedSecret) {
t.Errorf("Parsed SealedSecret does not match.\nExpected:\n%+v\nGot:\n%+v", *test.expectSealedSecret, sealedSecret)
t.Errorf("(Test %d) Parsed SealedSecret does not match.\nExpected:\n%+v\nGot:\n%+v", i, *test.expectSealedSecret, sealedSecret)
}
}
}
Loading

0 comments on commit a01190c

Please sign in to comment.