1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

feat: add ability to push expiration date to secret in azure key vault (#4149)

* feat: add ability to push expiration date of secret to azure key vault with annotation

Signed-off-by: deggja <danieldagfinrud@gmail.com>

* docs: set example annotation on secret in docs

Signed-off-by: deggja <danieldagfinrud@gmail.com>

* test: added test for updating to new expiration date

Signed-off-by: deggja <danieldagfinrud@gmail.com>

* chore: format

Signed-off-by: deggja <danieldagfinrud@gmail.com>

* chore: clean up go.mod

Signed-off-by: deggja <danieldagfinrud@gmail.com>

* feat: add expiration date for secret as field in metadata block in pushsecret

Signed-off-by: deggja <danieldagfinrud@gmail.com>

* extract the metadata from Kubernetes package and put it into its own package

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: deggja <danieldagfinrud@gmail.com>
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
This commit is contained in:
Daniel R. Dagfinrud 2024-11-26 10:15:40 +01:00 committed by GitHub
parent b518bae15f
commit 40a698dafd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 157 additions and 47 deletions

View file

@ -23,4 +23,9 @@ spec:
- match:
secretKey: source-key # Source Kubernetes secret key containing the secret
remoteRef:
remoteKey: my-azkv-secret-name
remoteKey: my-azkv-secret-name
metadata:
apiVersion: kubernetes.external-secrets.io/v1alpha1
kind: PushSecretMetadata
spec:
expirationDate: "2024-12-31T23:59:59Z" # Expiration date for the secret in Azure Key Vault

6
go.mod
View file

@ -105,7 +105,7 @@ require (
cloud.google.com/go/auth v0.11.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
github.com/ProtonMail/go-crypto v1.1.2 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/gopenpgp/v2 v2.8.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
@ -160,7 +160,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
@ -260,7 +260,7 @@ require (
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0
k8s.io/gengo v0.0.0-20240911193312-2b36238f13e9 // indirect
k8s.io/klog v1.0.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect

4
go.sum
View file

@ -129,8 +129,8 @@ github.com/PaesslerAG/gval v1.2.3/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbV
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0Go3KS0=
github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.8.0 h1:WvMv3CMcFsqKSM4/Qf8sf3tgyQkzDqQmoSE49bnBuP4=

View file

@ -26,12 +26,14 @@ import (
"path"
"regexp"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
kvauth "github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/date"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/tidwall/gjson"
@ -52,6 +54,7 @@ import (
"github.com/external-secrets/external-secrets/pkg/constants"
"github.com/external-secrets/external-secrets/pkg/metrics"
"github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/metadata"
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
)
@ -119,6 +122,10 @@ type Azure struct {
namespace string
}
type PushSecretMetadataSpec struct {
ExpirationDate string `json:"expirationDate,omitempty"`
}
func init() {
esv1beta1.Register(&Azure{}, &esv1beta1.SecretStoreProvider{
AzureKV: &esv1beta1.AzureKVProvider{},
@ -411,7 +418,7 @@ func canCreate(tags map[string]*string, err error) (bool, error) {
return true, nil
}
func (a *Azure) setKeyVaultSecret(ctx context.Context, secretName string, value []byte) error {
func (a *Azure) setKeyVaultSecret(ctx context.Context, secretName string, value []byte, expires *date.UnixTime) error {
secret, err := a.baseClient.GetSecret(ctx, *a.provider.VaultURL, secretName, "")
metrics.ObserveAPICall(constants.ProviderAzureKV, constants.CallAzureKVGetSecret, err)
ok, err := canCreate(secret.Tags, err)
@ -423,8 +430,14 @@ func (a *Azure) setKeyVaultSecret(ctx context.Context, secretName string, value
}
val := string(value)
if secret.Value != nil && val == *secret.Value {
return nil
if secret.Attributes != nil {
if (secret.Attributes.Expires == nil && expires == nil) ||
(secret.Attributes.Expires != nil && expires != nil && *secret.Attributes.Expires == *expires) {
return nil
}
}
}
secretParams := keyvault.SecretSetParameters{
Value: &val,
Tags: map[string]*string{
@ -434,6 +447,11 @@ func (a *Azure) setKeyVaultSecret(ctx context.Context, secretName string, value
Enabled: pointer.To(true),
},
}
if expires != nil {
secretParams.SecretAttributes.Expires = expires
}
_, err = a.baseClient.SetSecret(ctx, *a.provider.VaultURL, secretName, secretParams)
metrics.ObserveAPICall(constants.ProviderAzureKV, constants.CallAzureKVGetSecret, err)
if err != nil {
@ -534,8 +552,9 @@ func (a *Azure) setKeyVaultKey(ctx context.Context, secretName string, value []b
// PushSecret stores secrets into a Key vault instance.
func (a *Azure) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
var (
value []byte
err error
value []byte
err error
expires *date.UnixTime
)
if data.GetSecretKey() == "" {
// Must convert secret values to string, otherwise data will be sent as base64 to Vault
@ -551,10 +570,24 @@ func (a *Azure) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1
value = secret.Data[data.GetSecretKey()]
}
metadata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](data.GetMetadata())
if err != nil {
return fmt.Errorf("failed to parse push secret metadata: %w", err)
}
if metadata != nil && metadata.Spec.ExpirationDate != "" {
t, err := time.Parse(time.RFC3339, metadata.Spec.ExpirationDate)
if err != nil {
return fmt.Errorf("error parsing expiration date in metadata: %w. Expected format: YYYY-MM-DDTHH:MM:SSZ (RFC3339). Example: 2024-12-31T20:00:00Z", err)
}
unixTime := date.UnixTime(t)
expires = &unixTime
}
objectType, secretName := getObjType(esv1beta1.ExternalSecretDataRemoteRef{Key: data.GetRemoteKey()})
switch objectType {
case defaultObjType:
return a.setKeyVaultSecret(ctx, secretName, value)
return a.setKeyVaultSecret(ctx, secretName, value, expires)
case objectTypeCert:
return a.setKeyVaultCertificate(ctx, secretName, value)
case objectTypeKey:

View file

@ -22,10 +22,14 @@ import (
"fmt"
"reflect"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/date"
"gopkg.in/yaml.v2"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
pointer "k8s.io/utils/ptr"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@ -33,6 +37,7 @@ import (
"github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault/fake"
testingfake "github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
"github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/metadata"
)
type secretManagerTestCase struct {
@ -65,6 +70,8 @@ type secretManagerTestCase struct {
expectedExistence bool
// for testing pushing multi-key k8s secrets
secret *corev1.Secret
// for testing changes in expiration date for akv secrets
newExpiry *date.UnixTime
}
func makeValidSecretManagerTestCase() *secretManagerTestCase {
@ -416,6 +423,45 @@ func TestAzureKeyVaultPushSecret(t *testing.T) {
Value: &goodSecret,
}
}
secretExpiryChange := func(smtc *secretManagerTestCase) {
newExpiry := date.UnixTime(time.Now().Add(24 * time.Hour))
oldExpiry := date.UnixTime(time.Now().Add(-1 * time.Hour))
mdata := &metadata.PushSecretMetadata[PushSecretMetadataSpec]{
APIVersion: metadata.APIVersion,
Kind: metadata.Kind,
Spec: PushSecretMetadataSpec{
ExpirationDate: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
},
}
metadataRaw, _ := yaml.Marshal(mdata)
smtc.newExpiry = &newExpiry
smtc.setValue = []byte(goodSecret)
smtc.pushData = testingfake.PushSecretData{
SecretKey: secretKey,
RemoteKey: secretName,
Metadata: &apiextensionsv1.JSON{
Raw: metadataRaw,
},
}
smtc.secretOutput = keyvault.SecretBundle{
Tags: map[string]*string{
"managed-by": pointer.To("external-secrets"),
},
Value: &goodSecret,
Attributes: &keyvault.SecretAttributes{
Expires: &oldExpiry,
},
}
smtc.setSecretOutput = keyvault.SecretBundle{
Tags: map[string]*string{
"managed-by": pointer.To("external-secrets"),
},
Value: &goodSecret,
Attributes: &keyvault.SecretAttributes{
Expires: smtc.newExpiry,
},
}
}
secretWrongTags := func(smtc *secretManagerTestCase) {
smtc.setValue = []byte(goodSecret)
smtc.pushData = testingfake.PushSecretData{
@ -814,6 +860,7 @@ func TestAzureKeyVaultPushSecret(t *testing.T) {
makeValidSecretManagerTestCaseCustom(wrongTags),
makeValidSecretManagerTestCaseCustom(secretSuccess),
makeValidSecretManagerTestCaseCustom(secretNoChange),
makeValidSecretManagerTestCaseCustom(secretExpiryChange),
makeValidSecretManagerTestCaseCustom(secretWrongTags),
makeValidSecretManagerTestCaseCustom(secretNoTags),
makeValidSecretManagerTestCaseCustom(secretNotFound),

View file

@ -34,6 +34,7 @@ import (
"github.com/external-secrets/external-secrets/pkg/find"
"github.com/external-secrets/external-secrets/pkg/metrics"
"github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/metadata"
)
const (
@ -133,7 +134,7 @@ func (c *Client) mergePushSecretData(remoteRef esv1beta1.PushSecretData, remoteS
remoteSecret.Data = make(map[string][]byte)
}
pushMeta, err := parseMetadataParameters(remoteRef.GetMetadata())
pushMeta, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](remoteRef.GetMetadata())
if err != nil {
return fmt.Errorf("unable to parse metadata parameters: %w", err)
}

View file

@ -18,20 +18,10 @@ import (
"fmt"
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"github.com/external-secrets/external-secrets/pkg/utils/metadata"
)
const (
metadataAPIVersion = "kubernetes.external-secrets.io/v1alpha1"
metadataKind = "PushSecretMetadata"
)
type PushSecretMetadata struct {
metav1.TypeMeta
Spec PushSecretMetadataSpec `json:"spec,omitempty"`
}
type PushSecretMetadataSpec struct {
TargetMergePolicy targetMergePolicy `json:"targetMergePolicy,omitempty"`
SourceMergePolicy sourceMergePolicy `json:"sourceMergePolicy,omitempty"`
@ -55,31 +45,10 @@ const (
sourceMergePolicyReplace sourceMergePolicy = "Replace"
)
func parseMetadataParameters(data *apiextensionsv1.JSON) (*PushSecretMetadata, error) {
if data == nil {
return nil, nil
}
var metadata PushSecretMetadata
err := yaml.Unmarshal(data.Raw, &metadata, yaml.DisallowUnknownFields)
if err != nil {
return nil, fmt.Errorf("failed to parse %s %s: %w", metadataAPIVersion, metadataKind, err)
}
if metadata.APIVersion != metadataAPIVersion {
return nil, fmt.Errorf("unexpected apiVersion %q, expected %q", metadata.APIVersion, metadataAPIVersion)
}
if metadata.Kind != metadataKind {
return nil, fmt.Errorf("unexpected kind %q, expected %q", metadata.Kind, metadataKind)
}
return &metadata, nil
}
// Takes the local secret metadata and merges it with the push metadata.
// The push metadata takes precedence.
// Depending on the policy, we either merge or overwrite the metadata from the local secret.
func mergeSourceMetadata(localSecret *v1.Secret, pushMeta *PushSecretMetadata) (map[string]string, map[string]string, error) {
func mergeSourceMetadata(localSecret *v1.Secret, pushMeta *metadata.PushSecretMetadata[PushSecretMetadataSpec]) (map[string]string, map[string]string, error) {
labels := localSecret.ObjectMeta.Labels
annotations := localSecret.ObjectMeta.Annotations
if pushMeta == nil {
@ -112,7 +81,7 @@ func mergeSourceMetadata(localSecret *v1.Secret, pushMeta *PushSecretMetadata) (
// Takes the remote secret metadata and merges it with the source metadata.
// The source metadata may replace the existing labels/annotations
// or merge into it depending on policy.
func mergeTargetMetadata(remoteSecret *v1.Secret, pushMeta *PushSecretMetadata, sourceLabels, sourceAnnotations map[string]string) (map[string]string, map[string]string, error) {
func mergeTargetMetadata(remoteSecret *v1.Secret, pushMeta *metadata.PushSecretMetadata[PushSecretMetadataSpec], sourceLabels, sourceAnnotations map[string]string) (map[string]string, map[string]string, error) {
labels := remoteSecret.ObjectMeta.Labels
annotations := remoteSecret.ObjectMeta.Annotations
if labels == nil {

View file

@ -0,0 +1,55 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metadata
import (
"fmt"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/yaml"
)
const (
APIVersion = "kubernetes.external-secrets.io/v1alpha1"
Kind = "PushSecretMetadata"
)
type PushSecretMetadata[T any] struct {
Kind string `json:"kind"`
APIVersion string `json:"apiVersion"`
Spec T `json:"spec,omitempty"`
}
// ParseMetadataParameters parses metadata with an arbitrary Spec.
func ParseMetadataParameters[T any](data *apiextensionsv1.JSON) (*PushSecretMetadata[T], error) {
if data == nil {
return nil, nil
}
var metadata PushSecretMetadata[T]
err := yaml.Unmarshal(data.Raw, &metadata, yaml.DisallowUnknownFields)
if err != nil {
return nil, fmt.Errorf("failed to parse %s %s: %w", APIVersion, Kind, err)
}
if metadata.APIVersion != APIVersion {
return nil, fmt.Errorf("unexpected apiVersion %q, expected %q", metadata.APIVersion, APIVersion)
}
if metadata.Kind != Kind {
return nil, fmt.Errorf("unexpected kind %q, expected %q", metadata.Kind, Kind)
}
return &metadata, nil
}