move cloud KMS providers to an external plugin

This commit is contained in:
Nicola Murino 2021-07-17 13:08:05 +02:00
parent 6d313f6d8f
commit 338301955f
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
11 changed files with 5 additions and 380 deletions

View file

@ -9,8 +9,6 @@ The `secrets` section of the `kms` configuration allows to configure how to encr
- `url` defines the URI to the KMS service
- `master_key_path` defines the absolute path to a file containing the master encryption key. This could be, for example, a docker secrets or a file protected with filesystem level permissions.
We use [Go CDK](https://gocloud.dev/howto/secrets/) to access several key management services in a portable way.
### Local provider
If the `url` is empty SFTPGo uses local encryption for keeping secrets. Internally, it uses the [NaCl secret box](https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox) algorithm to perform encryption and authentication.
@ -22,41 +20,9 @@ We first generate a random key, then the per-object encryption key is derived fr
For compatibility with SFTPGo versions 1.2.x and before we also support encryption based on `AES-256-GCM`. The data encrypted with this algorithm will never use the master key to keep backward compatibility.
### Google Cloud Key Management Service
### Cloud providers
To use keys from Google Cloud Platforms [Key Management Service](https://cloud.google.com/kms/) (GCP KMS) you have to use `gcpkms` as URL scheme like this:
```shell
gcpkms://projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]
```
SFTPGo will use Application Default Credentials. See [here](https://cloud.google.com/docs/authentication/production) for alternatives such as environment variables.
The URL host+path are used as the key resource ID; see [here](https://cloud.google.com/kms/docs/object-hierarchy#key) for more details.
If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using the Cloud provider and store this ciphertext.
### AWS Key Management Service
To use customer master keys from Amazon Web Services [Key Management Service](https://aws.amazon.com/kms/) (AWS KMS) you have to use `awskms` as URL scheme. You can use the keys ID, alias, or Amazon Resource Name (ARN) to identify the key. You should specify the region query parameter to ensure your application connects to the correct region.
Here are some examples:
- By ID: `awskms://1234abcd-12ab-34cd-56ef-1234567890ab?region=us-east-1`
- By alias: `awskms://alias/ExampleAlias?region=us-east-1`
- By ARN: `arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34bc-56ef-1234567890ab?region=us-east-1`
SFTPGo will use the default AWS session. See [AWS Session](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) to learn about authentication alternatives such as environment variables.
If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using the Cloud provider and store this ciphertext.
### HashiCorp Vault
To use the [transit secrets engine](https://www.vaultproject.io/docs/secrets/transit/index.html) in [Vault](https://www.vaultproject.io/) you have to use `hashivault` as URL scheme like this: `hashivault://mykey`.
The Vault server endpoint and authentication token are specified using the environment variables `VAULT_SERVER_URL` and `VAULT_SERVER_TOKEN`, respectively.
If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using Vault and store this ciphertext.
Several cloud providers are supported using the [sftpgo-plugin-kms](https://github.com/sftpgo/sftpgo-plugin-kms).
### Notes

View file

@ -1,67 +0,0 @@
// +build !noawskms
package aws
import (
// we import awskms here to be able to disable AWS KMS support using a build tag
_ "gocloud.dev/secrets/awskms"
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/kms/gocloud"
"github.com/drakkan/sftpgo/v2/version"
)
const encryptedStatus = kms.SecretStatusAWS
type awsSecret struct {
gocloud.Secret
}
func init() {
version.AddFeature("+awskms")
kms.RegisterSecretProvider(kms.SchemeAWS, encryptedStatus, newAWSSecret)
}
func newAWSSecret(base kms.BaseSecret, url, masterKey string) kms.SecretProvider {
return &awsSecret{
gocloud.Secret{
BaseSecret: base,
URL: url,
MasterKey: masterKey,
},
}
}
func (s *awsSecret) Name() string {
return "AWS"
}
func (s *awsSecret) IsEncrypted() bool {
return s.Status == encryptedStatus
}
func (s *awsSecret) Encrypt() error {
if err := s.Secret.Encrypt(); err != nil {
return err
}
s.Status = encryptedStatus
return nil
}
func (s *awsSecret) Decrypt() error {
if !s.IsEncrypted() {
return kms.ErrWrongSecretStatus
}
return s.Secret.Decrypt()
}
func (s *awsSecret) Clone() kms.SecretProvider {
baseSecret := kms.BaseSecret{
Status: s.Status,
Payload: s.Payload,
Key: s.Key,
AdditionalData: s.AdditionalData,
Mode: s.Mode,
}
return newAWSSecret(baseSecret, s.URL, s.MasterKey)
}

View file

@ -1,11 +0,0 @@
// +build noawskms
package aws
import (
"github.com/drakkan/sftpgo/v2/version"
)
func init() {
version.AddFeature("-awskms")
}

View file

@ -1,67 +0,0 @@
// +build !nogcpkms
package gcp
import (
// we import gcpkms here to be able to disable GCP KMS support using a build tag
_ "gocloud.dev/secrets/gcpkms"
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/kms/gocloud"
"github.com/drakkan/sftpgo/v2/version"
)
const encryptedStatus = kms.SecretStatusGCP
type gcpSecret struct {
gocloud.Secret
}
func init() {
version.AddFeature("+gcpkms")
kms.RegisterSecretProvider(kms.SchemeGCP, encryptedStatus, newGCPSecret)
}
func newGCPSecret(base kms.BaseSecret, url, masterKey string) kms.SecretProvider {
return &gcpSecret{
gocloud.Secret{
BaseSecret: base,
URL: url,
MasterKey: masterKey,
},
}
}
func (s *gcpSecret) Name() string {
return "GCP"
}
func (s *gcpSecret) IsEncrypted() bool {
return s.Status == encryptedStatus
}
func (s *gcpSecret) Encrypt() error {
if err := s.Secret.Encrypt(); err != nil {
return err
}
s.Status = encryptedStatus
return nil
}
func (s *gcpSecret) Decrypt() error {
if !s.IsEncrypted() {
return kms.ErrWrongSecretStatus
}
return s.Secret.Decrypt()
}
func (s *gcpSecret) Clone() kms.SecretProvider {
baseSecret := kms.BaseSecret{
Status: s.Status,
Payload: s.Payload,
Key: s.Key,
AdditionalData: s.AdditionalData,
Mode: s.Mode,
}
return newGCPSecret(baseSecret, s.URL, s.MasterKey)
}

View file

@ -1,11 +0,0 @@
// +build nogcpkms
package gcp
import (
"github.com/drakkan/sftpgo/v2/version"
)
func init() {
version.AddFeature("-gcpkms")
}

View file

@ -1,105 +0,0 @@
package gocloud
import (
"context"
"encoding/base64"
"time"
"gocloud.dev/secrets"
"github.com/drakkan/sftpgo/v2/kms"
)
const (
defaultTimeout = 10 * time.Second
)
// Secret defines common methods for go-cloud based kms
type Secret struct {
kms.BaseSecret
MasterKey string
URL string
}
func (s *Secret) Encrypt() error {
if s.Status != kms.SecretStatusPlain {
return kms.ErrWrongSecretStatus
}
if s.Payload == "" {
return kms.ErrInvalidSecret
}
payload := s.Payload
key := ""
mode := 0
if s.MasterKey != "" {
localSecret := kms.NewLocalSecret(s.BaseSecret, "", s.MasterKey)
err := localSecret.Encrypt()
if err != nil {
return err
}
payload = localSecret.GetPayload()
key = localSecret.GetKey()
mode = localSecret.GetMode()
}
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout))
defer cancelFn()
keeper, err := secrets.OpenKeeper(ctx, s.URL)
if err != nil {
return err
}
defer keeper.Close()
ciphertext, err := keeper.Encrypt(context.Background(), []byte(payload))
if err != nil {
return err
}
s.Payload = base64.StdEncoding.EncodeToString(ciphertext)
s.Key = key
s.Mode = mode
return nil
}
func (s *Secret) Decrypt() error {
encrypted, err := base64.StdEncoding.DecodeString(s.Payload)
if err != nil {
return err
}
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout))
defer cancelFn()
keeper, err := secrets.OpenKeeper(ctx, s.URL)
if err != nil {
return err
}
defer keeper.Close()
plaintext, err := keeper.Decrypt(context.Background(), encrypted)
if err != nil {
return err
}
payload := string(plaintext)
if s.Key != "" {
baseSecret := kms.BaseSecret{
Status: kms.SecretStatusSecretBox,
Payload: payload,
Key: s.Key,
AdditionalData: s.AdditionalData,
Mode: s.Mode,
}
localSecret := kms.NewLocalSecret(baseSecret, "", s.MasterKey)
err = localSecret.Decrypt()
if err != nil {
return err
}
payload = localSecret.GetPayload()
}
s.Status = kms.SecretStatusPlain
s.Payload = payload
s.Key = ""
s.AdditionalData = ""
s.Mode = 0
return nil
}

View file

@ -171,6 +171,7 @@ func (c *Configuration) getSecretProvider(base BaseSecret) SecretProvider {
return v.newFn(base, c.Secrets.URL, c.Secrets.masterKey)
}
}
logger.Warn(logSender, "", "no secret provider registered for URL %v, fallback to local provider", c.Secrets.URL)
return NewLocalSecret(base, c.Secrets.URL, c.Secrets.masterKey)
}

View file

@ -1,67 +0,0 @@
// +build !novaultkms
package vault
import (
// we import hashivault here to be able to disable Vault support using a build tag
_ "gocloud.dev/secrets/hashivault"
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/kms/gocloud"
"github.com/drakkan/sftpgo/v2/version"
)
const encryptedStatus = kms.SecretStatusVaultTransit
type vaultSecret struct {
gocloud.Secret
}
func init() {
version.AddFeature("+vaultkms")
kms.RegisterSecretProvider(kms.SchemeVaultTransit, encryptedStatus, newVaultSecret)
}
func newVaultSecret(base kms.BaseSecret, url, masterKey string) kms.SecretProvider {
return &vaultSecret{
gocloud.Secret{
BaseSecret: base,
URL: url,
MasterKey: masterKey,
},
}
}
func (s *vaultSecret) Name() string {
return "VaultTransit"
}
func (s *vaultSecret) IsEncrypted() bool {
return s.Status == encryptedStatus
}
func (s *vaultSecret) Encrypt() error {
if err := s.Secret.Encrypt(); err != nil {
return err
}
s.Status = encryptedStatus
return nil
}
func (s *vaultSecret) Decrypt() error {
if !s.IsEncrypted() {
return kms.ErrWrongSecretStatus
}
return s.Secret.Decrypt()
}
func (s *vaultSecret) Clone() kms.SecretProvider {
baseSecret := kms.BaseSecret{
Status: s.Status,
Payload: s.Payload,
Key: s.Key,
AdditionalData: s.AdditionalData,
Mode: s.Mode,
}
return newVaultSecret(baseSecret, s.URL, s.MasterKey)
}

View file

@ -1,11 +0,0 @@
// +build novaultkms
package vault
import (
"github.com/drakkan/sftpgo/v2/version"
)
func init() {
version.AddFeature("-vaultkms")
}

View file

@ -11,9 +11,6 @@ import (
"go.uber.org/automaxprocs/maxprocs"
"github.com/drakkan/sftpgo/v2/cmd"
_ "github.com/drakkan/sftpgo/v2/kms/aws"
_ "github.com/drakkan/sftpgo/v2/kms/gcp"
_ "github.com/drakkan/sftpgo/v2/kms/vault"
)
func main() {

View file

@ -22,11 +22,11 @@ type Info struct {
func GetAsString() string {
var sb strings.Builder
sb.WriteString(info.Version)
if len(info.CommitHash) > 0 {
if info.CommitHash != "" {
sb.WriteString("-")
sb.WriteString(info.CommitHash)
}
if len(info.BuildDate) > 0 {
if info.BuildDate != "" {
sb.WriteString("-")
sb.WriteString(info.BuildDate)
}