diff --git a/CHANGELOG.md b/CHANGELOG.md index 4afe668..40ab6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Renamed cert source `controller` to `kubernetes` - Moved `maxAge` one level up +- keys from snake_case to camelCase ### Fixed - Flaky verify command diff --git a/README.md b/README.md index bc7f184..e905037 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ The filename can be overwritten by setting the `--config` flag. A sample configuration file can be created via `sealit init`. ```yaml -sealing_rules: - - file_regex: \.dev\.yaml$ # Regex pattern for which files this rules are applied +sealingRules: + - fileRegex: \.dev\.yaml$ # Regex pattern for which files this rules are applied name: secret # Name of the future secret namespace: default # Namespace of the future secret - encrypt_regex: (password|pin)$ # Regex of the key names which should be encrypted + secretsRegex: (password|pin)$ # Regex of the key names which should be encrypted maxAge: 720h0m0s cert: url: https://example.org @@ -75,7 +75,7 @@ Otherwise the cert from the meta field within the `values.yaml` file is used for #### Local cert file ```yaml -sealing_rules: +sealingRules: - ... cert: ... @@ -85,7 +85,7 @@ sealing_rules: #### Remote cert file ```yaml -sealing_rules: +sealingRules: - ... cert: ... @@ -95,7 +95,7 @@ sealing_rules: #### Remote cert from Kubernetes ```yaml -sealing_rules: +sealingRules: - ... cert: ... diff --git a/example/.sealit.yaml b/example/.sealit.yaml index 41bed26..05a3c2c 100644 --- a/example/.sealit.yaml +++ b/example/.sealit.yaml @@ -1,15 +1,15 @@ -sealing_rules: - - file_regex: \.dev\.yaml$ +sealingRules: + - fileRegex: \.dev\.yaml$ name: mysecret namespace: default - encrypt_regex: (password|pin)$ + secretsRegex: (password|pin)$ maxAge: 720h cert: path: cert.pem - - file_regex: \.prod\.yaml$ + - fileRegex: \.prod\.yaml$ name: mysecret namespace: default - encrypt_regex: (password|pin)$ + secretsRegex: (password|pin)$ maxAge: 720h cert: path: cert.pem \ No newline at end of file diff --git a/example/values.dev.yaml b/example/values.dev.yaml index 5354bed..1c71110 100644 --- a/example/values.dev.yaml +++ b/example/values.dev.yaml @@ -2,4 +2,4 @@ env: password: secret! username: john secret: john - asd_username: john \ No newline at end of file + pin: 1234 \ No newline at end of file diff --git a/internal/config.go b/internal/config.go index fae23bf..3854f80 100644 --- a/internal/config.go +++ b/internal/config.go @@ -12,7 +12,7 @@ import ( var kubeConfig string type Config struct { - SealingRuleSets []SealingRuleSet `yaml:"sealing_rules"` + SealingRuleSets []SealingRuleSet `yaml:"sealingRules"` } // ExampleConfig Provide an example config of the `.sealit.yaml` @@ -25,7 +25,7 @@ func ExampleConfig() Config { FileRegex: "\\.dev\\.yaml$", Name: "secret", Namespace: "default", - EncryptRegex: "(password|pin)$", + SecretsRegex: "(password|pin)$", MaxAge: d, CertSources: CertSources{ Url: "https://example.org", diff --git a/internal/config_test.go b/internal/config_test.go index ab249fa..843ef83 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -3,11 +3,11 @@ package internal import "testing" var basicConfig = []byte(` -sealing_rules: - - file_regex: \.dev\.yaml$ +sealingRules: + - fileRegex: \.dev\.yaml$ name: mysecret namespace: default - encrypt_regex: (password|pin)$ + secretsRegex: (password|pin)$ maxAge: 720h cert: kubernetes: diff --git a/internal/sealer.go b/internal/sealer.go index 23daac9..b9b965f 100644 --- a/internal/sealer.go +++ b/internal/sealer.go @@ -23,11 +23,13 @@ const ( validCert ) +const encodeIdentifier = "ENC:" + type Sealer struct { - regexp *regexp.Regexp - publicKey *rsa.PublicKey - label []byte - metadata *Metadata + secretsRegexp *regexp.Regexp + publicKey *rsa.PublicKey + label []byte + metadata *Metadata } func NewSealer(srs *SealingRuleSet, m *Metadata, fetchCert bool) (s *Sealer, err error) { @@ -72,47 +74,54 @@ func NewSealer(srs *SealingRuleSet, m *Metadata, fetchCert bool) (s *Sealer, err } return &Sealer{ - regexp: regexp.MustCompile(srs.EncryptRegex), - publicKey: pKey, - label: m.getLabel(), - metadata: m, + secretsRegexp: srs.GetRegexForSecrets(), + publicKey: pKey, + label: m.getLabel(), + metadata: m, }, nil } +func (s *Sealer) valueNeedsToBeSealed(key *yaml.Node, value *yaml.Node) bool { + if s.secretsRegexp.MatchString(key.Value) { + if !strings.HasPrefix(value.Value, encodeIdentifier) { + return true + } + log.Printf("[DEBUG] Value of `%s` was already encrypted", key.Value) + + return false + } + log.Printf("[DEBUG] `%s` did not match regex %s", key.Value, s.secretsRegexp.String()) + + return false +} + func (s *Sealer) Verify(key *yaml.Node, value *yaml.Node) error { - if s.regexp.MatchString(key.Value) && !strings.HasPrefix(value.Value, "ENC:") { + if s.valueNeedsToBeSealed(key, value) { return fmt.Errorf("key `%s` is not encrypted", key.Value) } + return nil } func (s *Sealer) Seal(key *yaml.Node, value *yaml.Node) error { - if s.regexp.MatchString(key.Value) { - if !strings.HasPrefix(value.Value, "ENC:") { - ciphertext, err := crypto.HybridEncrypt(rand.Reader, s.publicKey, []byte(value.Value), s.label) - - if err != nil { - return err - } - - if value.Value == "" { - log.Printf("[WARNING] Value of `%s` is an empty string", key.Value) - } else if value.Value != strings.TrimSpace(value.Value) { - log.Printf("[WARNING] Value of `%s` is padded with whitespace", key.Value) - } + if s.valueNeedsToBeSealed(key, value) { + ciphertext, err := crypto.HybridEncrypt(rand.Reader, s.publicKey, []byte(value.Value), s.label) - encodedSecret := base64.StdEncoding.EncodeToString(ciphertext) - value.SetString(fmt.Sprintf("ENC:%s", encodedSecret)) - s.metadata.SealedAt = time.Now().Format(time.RFC3339) - log.Printf("[DEBUG] Encrypted value of `%s`", key.Value) + if err != nil { + return err + } - return nil + if value.Value == "" { + log.Printf("[WARNING] Value of `%s` is an empty string", key.Value) + } else if value.Value != strings.TrimSpace(value.Value) { + log.Printf("[WARNING] Value of `%s` is padded with whitespace", key.Value) } - log.Printf("[DEBUG] Value of `%s` was already encrypted", key.Value) - return nil + encodedSecret := base64.StdEncoding.EncodeToString(ciphertext) + value.SetString(fmt.Sprintf("%s%s", encodeIdentifier, encodedSecret)) + s.metadata.SealedAt = time.Now().Format(time.RFC3339) + log.Printf("[DEBUG] Encrypted value of `%s`", key.Value) } - log.Printf("[DEBUG] `%s` did not match regex %s", key.Value, s.regexp.String()) return nil } diff --git a/internal/sealer_test.go b/internal/sealer_test.go index d88b355..db9d4ee 100644 --- a/internal/sealer_test.go +++ b/internal/sealer_test.go @@ -17,9 +17,9 @@ func TestSealSecrets(t *testing.T) { key, _ := testGeneratePrivateKey() s := Sealer{ - regexp: regexp.MustCompile(`(password|pin)$`), - publicKey: &key.PublicKey, - metadata: &Metadata{}, + secretsRegexp: regexp.MustCompile(`(password|pin)$`), + publicKey: &key.PublicKey, + metadata: &Metadata{}, } k := &yaml.Node{Value: "test_password"} @@ -35,9 +35,9 @@ func TestVerifySecrets(t *testing.T) { key, _ := testGeneratePrivateKey() s := Sealer{ - regexp: regexp.MustCompile(`(password|pin)$`), - publicKey: &key.PublicKey, - metadata: &Metadata{}, + secretsRegexp: regexp.MustCompile(`(password|pin)$`), + publicKey: &key.PublicKey, + metadata: &Metadata{}, } k := &yaml.Node{Value: "test_password"} @@ -53,9 +53,9 @@ func TestVerifyUnsealedSecrets(t *testing.T) { key, _ := testGeneratePrivateKey() s := Sealer{ - regexp: regexp.MustCompile(`(password|pin)$`), - publicKey: &key.PublicKey, - metadata: &Metadata{}, + secretsRegexp: regexp.MustCompile(`(password|pin)$`), + publicKey: &key.PublicKey, + metadata: &Metadata{}, } k := &yaml.Node{Value: "test_password"} @@ -71,9 +71,9 @@ func TestSealingOfAlreadySealedSecrets(t *testing.T) { key, _ := testGeneratePrivateKey() s := Sealer{ - regexp: regexp.MustCompile(`(password|pin)$`), - publicKey: &key.PublicKey, - metadata: &Metadata{}, + secretsRegexp: regexp.MustCompile(`(password|pin)$`), + publicKey: &key.PublicKey, + metadata: &Metadata{}, } k := &yaml.Node{Value: "test_password"} @@ -89,9 +89,9 @@ func TestSealNonSecrets(t *testing.T) { key, _ := testGeneratePrivateKey() s := Sealer{ - regexp: regexp.MustCompile(`(password|pin)$`), - publicKey: &key.PublicKey, - metadata: &Metadata{}, + secretsRegexp: regexp.MustCompile(`(password|pin)$`), + publicKey: &key.PublicKey, + metadata: &Metadata{}, } k := &yaml.Node{Value: "test"} diff --git a/internal/sealing_rule_set.go b/internal/sealing_rule_set.go index 13a0b5f..c19418a 100644 --- a/internal/sealing_rule_set.go +++ b/internal/sealing_rule_set.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "os" + "regexp" "time" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" @@ -22,10 +23,10 @@ type certSource interface { } type SealingRuleSet struct { - FileRegex string `yaml:"file_regex"` + FileRegex string `yaml:"fileRegex"` Name string `yaml:"name"` Namespace string `yaml:"namespace"` - EncryptRegex string `yaml:"encrypt_regex"` + SecretsRegex string `yaml:"secretsRegex"` MaxAge time.Duration `yaml:"maxAge"` CertSources CertSources `yaml:"cert"` } @@ -46,6 +47,10 @@ type KubernetesCertSource struct { Namespace string `yaml:"namespace"` } +func (srs *SealingRuleSet) GetRegexForSecrets() *regexp.Regexp { + return regexp.MustCompile(srs.SecretsRegex) +} + // GetCert fetches the cert from different sources // Prio: // 1. fetch from Kubernetes cluster