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

Add support for Azure Keyvault #493

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ bin/
config/release/
config/crd/bases/gitrepositories.yaml
config/crd/bases/buckets.yaml

kustomize-controller
5 changes: 5 additions & 0 deletions controllers/kustomization_decryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ func (kd *KustomizeDecryptor) ImportKeys(ctx context.Context) error {
if err := kd.gpgImport(keyPath); err != nil {
return err
}
case ".json":
keyPath := filepath.Join(kd.homeDir, name)
if err := os.WriteFile(keyPath, file, os.ModePerm); err != nil {
return fmt.Errorf("unable to write key to storage: %w", err)
}
case ".agekey":
ageIdentities = append(ageIdentities, string(file))
}
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ replace github.com/fluxcd/kustomize-controller/api => ./api

require (
filippo.io/age v1.0.0
github.com/Azure/azure-sdk-for-go v57.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.22 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 // indirect
github.com/cyphar/filepath-securejoin v0.2.2
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/drone/envsubst v1.0.3-0.20200804185402-58bc65f69603
github.com/fluxcd/kustomize-controller/api v0.18.0
github.com/fluxcd/pkg/apis/kustomize v0.2.0
Expand All @@ -21,7 +25,7 @@ require (
github.com/onsi/gomega v1.15.0
github.com/spf13/pflag v1.0.5
go.mozilla.org/sops/v3 v3.7.1
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
google.golang.org/grpc v1.42.0
k8s.io/api v0.22.2
Expand Down
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCO
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/Azure/azure-sdk-for-go v31.2.0+incompatible h1:kZFnTLmdQYNGfakatSivKHUfUnDZhqNdchHD4oIhp5k=
github.com/Azure/azure-sdk-for-go v31.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v57.2.0+incompatible h1:zoJapafogLazoyp0x9aQENzNNqxvU6pnGtb2P8/i+HI=
github.com/Azure/azure-sdk-for-go v57.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
Expand All @@ -43,15 +45,25 @@ github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+B
github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM=
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest v0.11.19 h1:7/IqD2fEYVha1EPeaiytVKhzmPV223pfkRIQUGOK2IE=
github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest v0.11.22 h1:bXiQwDjrRmBQOE67bwlvUKAC1EU1yZTPQ38c+bstZws=
github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs=
github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q=
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
github.com/Azure/go-autorest/autorest/adal v0.9.14 h1:G8hexQdV5D4khOXrWG2YuLCFKhWYmWD8bHYaXN5ophk=
github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
github.com/Azure/go-autorest/autorest/azure/auth v0.1.0 h1:YgO/vSnJEc76NLw2ecIXvXa8bDWiqf1pOJzARAoZsYU=
github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 h1:Y2CgdzitFDsdMwYMzf9LIZWrrTFysqbRc7b94XVVJ78=
github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU=
github.com/Azure/go-autorest/autorest/azure/cli v0.1.0 h1:YTtBrcb6mhA+PoSW8WxFDoIIyjp13XqJeX80ssQtri4=
github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
Expand Down Expand Up @@ -181,6 +193,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
Expand Down Expand Up @@ -810,6 +824,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down
171 changes: 171 additions & 0 deletions internal/sops/keyservice/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
package keyservice

import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"unicode/utf16"

"go.mozilla.org/sops/v3/keyservice"
"golang.org/x/net/context"
Expand All @@ -14,6 +23,12 @@ import (

"github.com/fluxcd/kustomize-controller/internal/sops/age"
"github.com/fluxcd/kustomize-controller/internal/sops/pgp"

"github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/dimchansky/utfbom"
)

// Server is a key service server that uses SOPS MasterKeys to fulfill
Expand Down Expand Up @@ -86,6 +101,138 @@ func (ks *Server) decryptWithAge(key *keyservice.AgeKey, ciphertext []byte) ([]b
return plaintext, err
}

func (ks *Server) newKeyvaultAuthorizerFromFile(fileLocation string) (autorest.Authorizer, error) {
s := auth.FileSettings{}
s.Values = map[string]string{}

contents, err := ioutil.ReadFile(fileLocation)
if err != nil {
return nil, err
}

// Auth file might be encoded
var decoded []byte
reader, enc := utfbom.Skip(bytes.NewReader(contents))
switch enc {
case utfbom.UTF16LittleEndian:
u16 := make([]uint16, (len(contents)/2)-1)
err := binary.Read(reader, binary.LittleEndian, &u16)
if err != nil {
return nil, err
}
decoded = []byte(string(utf16.Decode(u16)))
case utfbom.UTF16BigEndian:
u16 := make([]uint16, (len(contents)/2)-1)
err := binary.Read(reader, binary.BigEndian, &u16)
if err != nil {
return nil, err
}
decoded = []byte(string(utf16.Decode(u16)))
default:
decoded, err = ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
}

// unmarshal file
authFile := map[string]interface{}{}
err = json.Unmarshal(decoded, &authFile)
if err != nil {
return nil, err
}

if val, ok := authFile["clientId"]; ok {
s.Values["AZURE_CLIENT_ID"] = val.(string)
}
if val, ok := authFile["clientSecret"]; ok {
s.Values["AZURE_CLIENT_SECRET"] = val.(string)
}
if val, ok := authFile["clientCertificate"]; ok {
s.Values["AZURE_CERTIFICATE_PATH"] = val.(string)
}
if val, ok := authFile["clientCertificatePassword"]; ok {
s.Values["AZURE_CERTIFICATE_PASSWORD"] = val.(string)
}
if val, ok := authFile["subscriptionId"]; ok {
s.Values["AZURE_SUBSCRIPTION_ID"] = val.(string)
}
if val, ok := authFile["tenantId"]; ok {
s.Values["AZURE_TENANT_ID"] = val.(string)
}
if val, ok := authFile["activeDirectoryEndpointUrl"]; ok {
s.Values["ActiveDirectoryEndpoint"] = val.(string)
}
if val, ok := authFile["resourceManagerEndpointUrl"]; ok {
s.Values["ResourceManagerEndpoint"] = val.(string)
}
if val, ok := authFile["activeDirectoryGraphResourceId"]; ok {
s.Values["GraphResourceID"] = val.(string)
}
if val, ok := authFile["sqlManagementEndpointUrl"]; ok {
s.Values["SQLManagementEndpoint"] = val.(string)
}
if val, ok := authFile["galleryEndpointUrl"]; ok {
s.Values["GalleryEndpoint"] = val.(string)
}
if val, ok := authFile["managementEndpointUrl"]; ok {
s.Values["ManagementEndpoint"] = val.(string)
}

resource := azure.PublicCloud.ResourceIdentifiers.KeyVault
if a, err := s.ClientCredentialsAuthorizerWithResource(resource); err == nil {
return a, err
}
if a, err := s.ClientCertificateAuthorizerWithResource(resource); err == nil {
return a, err
}
return nil, errors.New("auth file missing client and certificate credentials")
}

func (ks *Server) encryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, plaintext []byte) ([]byte, error) {
var err error

kv := keyvault.New()
if kv.Authorizer, err = ks.newKeyvaultAuthorizerFromFile(filepath.Join(ks.HomeDir, "azure_kv.json")); err != nil {
return nil, err
}

plainstring := string(plaintext)
result, err := kv.Encrypt(context.Background(), key.VaultUrl, key.Name, "", keyvault.KeyOperationsParameters{Algorithm: keyvault.RSAOAEP256, Value: &plainstring})
if err != nil {
return nil, fmt.Errorf("Failed to encrypt data: %w", err)
}

ciphertext, err := base64.RawURLEncoding.DecodeString(*result.Result)
if err != nil {
return nil, fmt.Errorf("Failed to encrypt data: %w", err)
}

return ciphertext, nil
}

func (ks *Server) decryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, ciphertext []byte) ([]byte, error) {
var err error

kv := keyvault.New()
if kv.Authorizer, err = ks.newKeyvaultAuthorizerFromFile(filepath.Join(ks.HomeDir, "azure_kv.json")); err != nil {
return nil, err
}

cipherstring := string(ciphertext)
result, err := kv.Decrypt(context.Background(), key.VaultUrl, key.Name, key.Version, keyvault.KeyOperationsParameters{Algorithm: keyvault.RSAOAEP256, Value: &cipherstring})
if err != nil {
return nil, fmt.Errorf("Failed to decrypt data: %w", err)
}

plaintext, err := base64.RawURLEncoding.DecodeString(*result.Result)
if err != nil {
return nil, fmt.Errorf("Failed to decrypt data: %w", err)
}

return plaintext, nil
}

// Encrypt takes an encrypt request and encrypts the provided plaintext with the provided key,
// returning the encrypted result.
func (ks Server) Encrypt(ctx context.Context,
Expand All @@ -109,6 +256,18 @@ func (ks Server) Encrypt(ctx context.Context,
response = &keyservice.EncryptResponse{
Ciphertext: ciphertext,
}
case *keyservice.Key_AzureKeyvaultKey:
if _, err := os.Stat(filepath.Join(ks.HomeDir, "azure_kv.json")); os.IsNotExist(err) {
stefanprodan marked this conversation as resolved.
Show resolved Hide resolved
return ks.Encrypt(ctx, req)
} else {
ciphertext, err := ks.encryptWithAzureKeyvault(k.AzureKeyvaultKey, req.Plaintext)
if err != nil {
return nil, err
}
response = &keyservice.EncryptResponse{
Ciphertext: ciphertext,
}
}
default:
return ks.Encrypt(ctx, req)
}
Expand Down Expand Up @@ -169,6 +328,18 @@ func (ks Server) Decrypt(ctx context.Context,
response = &keyservice.DecryptResponse{
Plaintext: plaintext,
}
case *keyservice.Key_AzureKeyvaultKey:
if _, err := os.Stat(filepath.Join(ks.HomeDir, "azure_kv.json")); os.IsNotExist(err) {
return ks.DefaultServer.Decrypt(ctx, req)
} else {
plaintext, err := ks.decryptWithAzureKeyvault(k.AzureKeyvaultKey, req.Ciphertext)
if err != nil {
return nil, err
}
response = &keyservice.DecryptResponse{
Plaintext: plaintext,
}
}
default:
return ks.DefaultServer.Decrypt(ctx, req)
}
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
flag "github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
ctrl "sigs.k8s.io/controller-runtime"
crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
Expand Down