Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow providing cluster-wide age identity via controller env var #918

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions internal/decryptor/decryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/resource"
Expand Down Expand Up @@ -194,13 +195,26 @@ func IsEncryptedSecret(object *unstructured.Unstructured) bool {
// For the import of PGP keys, the Decryptor must be configured with
// an absolute GnuPG home directory path.
func (d *Decryptor) ImportKeys(ctx context.Context) error {
if d.kustomization.Spec.Decryption == nil || d.kustomization.Spec.Decryption.SecretRef == nil {
if d.kustomization.Spec.Decryption == nil {
return nil
}

provider := d.kustomization.Spec.Decryption.Provider
switch provider {
case DecryptionProviderSOPS:
// load age key from env variable
globalAgeIdentities, err := age.GlobalIdentities()
if err != nil {
log := ctrl.LoggerFrom(ctx)
log.Info("failed to decrypt age identity from environment, ignoring", "error", err)
} else {
d.ageIdentities = append(d.ageIdentities, globalAgeIdentities...)
}

if d.kustomization.Spec.Decryption.SecretRef == nil {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the case where the env var is not set and no secret is provided in the Kustomization, this just bails out but it should actually return an error because providing a decryption provider without any keys is a user error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that behaviour was there before, i did not introduce it?

}

secretName := types.NamespacedName{
Namespace: d.kustomization.GetNamespace(),
Name: d.kustomization.Spec.Decryption.SecretRef.Name,
Expand All @@ -214,7 +228,6 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
return fmt.Errorf("cannot get %s decryption Secret '%s': %w", provider, secretName, err)
}

var err error
for name, value := range secret.Data {
switch filepath.Ext(name) {
case DecryptionPGPExt:
Expand Down
62 changes: 62 additions & 0 deletions internal/decryptor/decryptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func TestDecryptor_ImportKeys(t *testing.T) {
secret *corev1.Secret
wantErr bool
inspectFunc func(g *GomegaWithT, decryptor *Decryptor)
env map[string]string
}{
{
name: "PGP key",
Expand Down Expand Up @@ -169,6 +170,54 @@ func TestDecryptor_ImportKeys(t *testing.T) {
g.Expect(decryptor.ageIdentities).To(HaveLen(0))
},
},
{
name: "age key from env",
decryption: &kustomizev1.Decryption{
Provider: provider,
},
env: map[string]string{
"FLUX_SOPS_AGE_KEY": string(ageKey),
},
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.ageIdentities).To(HaveLen(1))
},
},
{
name: "age key from env invalid",
decryption: &kustomizev1.Decryption{
Provider: provider,
},
env: map[string]string{
"FLUX_SOPS_AGE_KEY": "invalid-key",
},
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.ageIdentities).To(HaveLen(0))
},
},
{
name: "age key from env and secret",
decryption: &kustomizev1.Decryption{
Provider: provider,
SecretRef: &meta.LocalObjectReference{
Name: "age-secret",
},
},
env: map[string]string{
"FLUX_SOPS_AGE_KEY": string(ageKey),
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "age-secret",
Namespace: provider,
},
Data: map[string][]byte{
"age" + DecryptionAgeExt: ageKey,
},
},
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.ageIdentities).To(HaveLen(2))
},
},
{
name: "HC Vault token",
decryption: &kustomizev1.Decryption{
Expand Down Expand Up @@ -376,6 +425,19 @@ clientSecret: some-client-secret`),
},
}

for envName, envVal := range tt.env {
cleanName := envName
prevValue, wasPresent := os.LookupEnv(envName)
t.Cleanup(func() {
if wasPresent {
os.Setenv(cleanName, prevValue)
} else {
os.Unsetenv(cleanName)
}
})
os.Setenv(envName, envVal)
}

d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization)
g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup)
Expand Down
20 changes: 20 additions & 0 deletions internal/sops/age/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,32 @@ import (
"bytes"
"fmt"
"io"
"os"
"strings"

"filippo.io/age"
"filippo.io/age/armor"
)

const (
// SopsAgeKeyEnv can be set as an environment variable to provide
// an additional key to use for decryption.
SopsAgeKeyEnv = "FLUX_SOPS_AGE_KEY"
)

// GlobalIdentities loads age identities from the [SopsAgeKeyEnv] environment variable.
func GlobalIdentities() ([]age.Identity, error) {
if globalKey, ok := os.LookupEnv(SopsAgeKeyEnv); ok {
parsed, err := age.ParseIdentities(strings.NewReader(globalKey))
if err != nil {
return nil, fmt.Errorf("failed to parse age identities from env var: %w", err)
}
return parsed, nil
}

return nil, nil
}

// MasterKey is an age key used to Encrypt and Decrypt SOPS' data key.
//
// Adapted from https://github.com/mozilla/sops/blob/v3.7.2/age/keysource.go
Expand Down