Skip to content

Commit

Permalink
support for auto-encrypting fields (#708)
Browse files Browse the repository at this point in the history
  • Loading branch information
ssoroka authored Dec 7, 2021
1 parent 1e7bd82 commit a024207
Show file tree
Hide file tree
Showing 32 changed files with 882 additions and 114 deletions.
48 changes: 47 additions & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,57 @@ When deploying Infra, we recommend Infra be deployed in its own namespace to min

## Sensitive Information

These secrets can be stored in a variety of [secret storage backends](secrets.md), including Kubernetes secrets, Vault, AWS Secrets Manager, AWS SSM (Systems Manager Parameter Store), and some simple options exist for loading secrets from the OS or container, such as: loading secrets from environment variables, loading secrets from files on the file system, and even plaintext secrets directly in the configuration file (though this is not recommended). With all types except for `plaintext`, the respective secret object names are specified in the configuration file and the actual secret is never persisted in Infra's storage. In the case of all secret types (including `plaintext`), the secret data is [encrypted at rest in the db](#Encrypted_At_Rest).

### Okta secrets
Infra uses an Okta application client secret and API token in order to allow users to authenticate via an OpenID Connect (OIDC) authorization code flow. These secrets are stored using Kubernetes secrets. Their respective secret object names are specified in the configuration file and the actual secret is never persisted in Infra's storage.
Infra uses an Okta application client secret and API token in order to allow users to authenticate via an OpenID Connect (OIDC) authorization code flow.

#### Okta client secret usage:
The client secret is loaded server-side from the specified Kubernetes secret only when a user is logging in via Okta.

#### Okta API token usage:
The Okta API token is only used for read actions. It is retrieved from the kubernetes secret when validating that the Okta connection is valid and when syncing users/groups.

## Encrypted At Rest

Sensitive data is always encrypted at rest in the db using a symmetric key. The symmetric key is stored in the database encrypted by a root key. By default this root key is generated by Infra and stored in Kubernetes secret storage under `infra-x/__root_key`. We strongly recommend enabling encryption-at-rest within Kubernetes for Kubernetes Secrets, or configuring another key provider service such as KMS or Vault.

The process of retrieving the db key is to load the encrypted key from the database, request that the db key be decrypted by the root key, and at which point the db key is used to decrypt all the data. In the case of AWS KMS and Vault, the Infra app never sees the root key, and so these options are preferred over the default built-in `native` key provider.

### Root key configuration examples

Infra uses AWS KMS key service

```yaml
config:
keys:
- kind: awskms
endpoint: https://your.kms.aws.url.example.com
region: us-east-1
accessKeyId: kubernetes:awskms/accessKeyID
secretAccessKey: kubernetes:awskms/secretAccessKey
encryptionAlgorithm: AES_256
```
Infra uses Vault as a key service
```yaml
config:
keys:
- kind: vault
address: https://your.vault.url.example.com
transitMount: /transit
token: kubernetes:vault/token
namespace:
```
Another alternative is that Infra can manages its own keys but store them in a secret storage service other than kubernetes. Valid secretStorage values are any secrets `name` that is already configured in config under `secrets`, as long as it's one of the types: `vault`, `awsssm`, `awssecretsmanager`, or `kubernetes`.

If you're using Kubernetes for root key storage, as the config here shows, you should back up the secret offline or to secret storage such as 1Password. If you don't want to be responsible for backing up this key, switch to Vault or KMS (examples above).

```yaml
config:
keys:
- kind: native
secretStorage: kubernetes
```
2 changes: 1 addition & 1 deletion helm/charts/infra/templates/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ metadata:
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
verbs: ["get", "create", "update", "patch"]
4 changes: 3 additions & 1 deletion internal/registry/_testdata/infra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ secrets:
- name: base64
kind: plaintext
base64: true

keys:
- kind: native
secretStorage: file
providers:
- kind: okta
domain: https://test.okta.com
Expand Down
2 changes: 1 addition & 1 deletion internal/registry/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ func (a *API) Login(c *gin.Context) {

provider := providers[0]

clientSecret, err := a.registry.GetSecret(provider.ClientSecret)
clientSecret, err := a.registry.GetSecret(string(provider.ClientSecret))
if err != nil {
sendAPIError(c, http.StatusBadRequest, err)
return
Expand Down
14 changes: 7 additions & 7 deletions internal/registry/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func TestCreateDestination(t *testing.T) {

for k, v := range cases {
t.Run(k, func(t *testing.T) {
db := configure(t, nil)
_, db := configure(t, nil)

requestFunc, ok := v["requestFunc"].(func(*testing.T) *api.DestinationCreateRequest)
require.True(t, ok)
Expand Down Expand Up @@ -185,7 +185,7 @@ func TestCreateDestination(t *testing.T) {
}

func TestCreateDestinationUpdatesField(t *testing.T) {
db := configure(t, nil)
_, db := configure(t, nil)

destination, err := data.CreateDestination(db, &models.Destination{
Kind: models.DestinationKindKubernetes,
Expand Down Expand Up @@ -327,7 +327,7 @@ func TestLogin(t *testing.T) {

for k, v := range cases {
t.Run(k, func(t *testing.T) {
db := configure(t, nil)
_, db := configure(t, nil)

requestFunc, ok := v["requestFunc"].(func(*testing.T) *http.Request)
require.True(t, ok)
Expand All @@ -352,7 +352,7 @@ func TestLogin(t *testing.T) {
}

func TestLoginOkta(t *testing.T) {
db := configure(t, nil)
_, db := configure(t, nil)

testOkta := new(mocks.Okta)
testOkta.On("EmailFromCode", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("jbond@infrahq.com", nil)
Expand Down Expand Up @@ -1216,7 +1216,7 @@ func TestT(t *testing.T) {

for k, v := range cases {
t.Run(k, func(t *testing.T) {
db := configure(t, nil)
_, db := configure(t, nil)

_, err := data.InitializeSettings(db)
require.NoError(t, err)
Expand Down Expand Up @@ -1251,7 +1251,7 @@ func TestT(t *testing.T) {
}

func TestCreateAPIKey(t *testing.T) {
db := configure(t, nil)
_, db := configure(t, nil)

request := api.InfraAPIKeyCreateRequest{
Name: "tmp",
Expand Down Expand Up @@ -1291,7 +1291,7 @@ func TestCreateAPIKey(t *testing.T) {
}

func TestDeleteAPIKey(t *testing.T) {
db := configure(t, nil)
_, db := configure(t, nil)

permissions := strings.Join([]string{
string(access.PermissionUserRead),
Expand Down
149 changes: 141 additions & 8 deletions internal/registry/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,63 @@ type ConfigSecretProvider struct {
Config interface{} // contains secret-provider-specific config
}

type ConfigSecretKeyProvider struct {
Kind string `yaml:"kind" validate:"required"`
Config interface{} // contains secret-provider-specific config
}

type simpleConfigSecretProvider struct {
Kind string `yaml:"kind"`
Name string `yaml:"name"`
}

// ensure ConfigSecretProvider implements yaml.Unmarshaller for the custom config field support
var _ yaml.Unmarshaler = &ConfigSecretProvider{}
// ensure these implements yaml.Unmarshaller for the custom config field support
var (
_ yaml.Unmarshaler = &ConfigSecretProvider{}
_ yaml.Unmarshaler = &ConfigSecretKeyProvider{}
)

func (sp *ConfigSecretKeyProvider) UnmarshalYAML(unmarshal func(interface{}) error) error {
tmp := &simpleConfigSecretProvider{}

if err := unmarshal(&tmp); err != nil {
return fmt.Errorf("unmarshalling secret provider: %w", err)
}

sp.Kind = tmp.Kind

switch sp.Kind {
case "vault":
p := secrets.NewVaultConfig()
if err := unmarshal(&p); err != nil {
return fmt.Errorf("unmarshal yaml: %w", err)
}

sp.Config = p
case "awskms":
p := secrets.NewAWSKMSConfig()
if err := unmarshal(&p); err != nil {
return fmt.Errorf("unmarshal yaml: %w", err)
}

if err := unmarshal(&p.AWSConfig); err != nil {
return fmt.Errorf("unmarshal yaml: %w", err)
}

sp.Config = p
case "native":
p := nativeSecretProviderConfig{}
if err := unmarshal(&p); err != nil {
return fmt.Errorf("unmarshal yaml: %w", err)
}

sp.Config = p
default:
return fmt.Errorf("unknown key provider type %q, expected one of %q", sp.Kind, secrets.SymmetricKeyProviderKinds)
}

return nil
}

func (sp *ConfigSecretProvider) UnmarshalYAML(unmarshal func(interface{}) error) error {
tmp := &simpleConfigSecretProvider{}
Expand Down Expand Up @@ -193,10 +243,11 @@ func (sp *ConfigSecretProvider) UnmarshalYAML(unmarshal func(interface{}) error)
}

type Config struct {
Secrets []ConfigSecretProvider `yaml:"secrets" validate:"dive"`
Providers []ConfigProvider `yaml:"providers" validate:"dive"`
Groups []ConfigGroupMapping `yaml:"groups" validate:"dive"`
Users []ConfigUserMapping `yaml:"users" validate:"dive"`
Secrets []ConfigSecretProvider `yaml:"secrets" validate:"dive"`
Keys []ConfigSecretKeyProvider `yaml:"keys" validate:"dive"`
Providers []ConfigProvider `yaml:"providers" validate:"dive"`
Groups []ConfigGroupMapping `yaml:"groups" validate:"dive"`
Users []ConfigUserMapping `yaml:"users" validate:"dive"`
}

func importProviders(db *gorm.DB, providers []ConfigProvider) error {
Expand All @@ -214,7 +265,7 @@ func importProviders(db *gorm.DB, providers []ConfigProvider) error {
Kind: models.ProviderKind(p.Kind),
Domain: p.Domain,
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
ClientSecret: models.EncryptedAtRest(p.ClientSecret),
}

switch provider.Kind {
Expand All @@ -224,7 +275,7 @@ func importProviders(db *gorm.DB, providers []ConfigProvider) error {
return fmt.Errorf("expected provider config to be Okta, but was %t", p.Config)
}

provider.Okta.APIToken = cfg.APIToken
provider.Okta.APIToken = models.EncryptedAtRest(cfg.APIToken)

default:
// should never happen
Expand Down Expand Up @@ -417,6 +468,10 @@ func (r *Registry) importSecretsConfig(bs []byte) error {
return fmt.Errorf("secrets config: %w", err)
}

if err := r.configureSecretKeys(config); err != nil {
return fmt.Errorf("secrets config: %w", err)
}

return nil
}

Expand Down Expand Up @@ -505,6 +560,84 @@ func isABaseSecretStorageKind(s string) bool {
return false
}

type nativeSecretProviderConfig struct {
SecretStorageName string `yaml:"secretStorage"`
}

func (r *Registry) configureSecretKeys(config Config) error {
var err error

if r.keyProvider == nil {
r.keyProvider = map[string]secrets.SymmetricKeyProvider{}
}

// default
r.keyProvider["native"] = secrets.NewNativeSecretProvider(r.secrets["kubernetes"])
r.keyProvider["default"] = r.keyProvider["native"]

for _, keyConfig := range config.Keys {
switch keyConfig.Kind {
case "native":
cfg, ok := keyConfig.Config.(nativeSecretProviderConfig)
if !ok {
return fmt.Errorf("expected key config to be NativeSecretProviderConfig, but was %t", keyConfig.Config)
}

storageProvider, found := r.secrets[cfg.SecretStorageName]
if !found {
return fmt.Errorf("secret storage name %q not found", cfg.SecretStorageName)
}

sp := secrets.NewNativeSecretProvider(storageProvider)
r.keyProvider[keyConfig.Kind] = sp
r.keyProvider["default"] = sp
case "awskms":
cfg, ok := keyConfig.Config.(secrets.AWSKMSConfig)
if !ok {
return fmt.Errorf("expected key config to be AWSKMSConfig, but was %t", keyConfig.Config)
}

cfg.AccessKeyID, err = r.GetSecret(cfg.AccessKeyID)
if err != nil {
return fmt.Errorf("getting secret for awskms accessKeyID: %w", err)
}

cfg.SecretAccessKey, err = r.GetSecret(cfg.SecretAccessKey)
if err != nil {
return fmt.Errorf("getting secret for awskms secretAccessKey: %w", err)
}

sp, err := secrets.NewAWSKMSSecretProviderFromConfig(cfg)
if err != nil {
return err
}

r.keyProvider[keyConfig.Kind] = sp
r.keyProvider["default"] = sp
case "vault":
cfg, ok := keyConfig.Config.(secrets.VaultConfig)
if !ok {
return fmt.Errorf("expected key config to be VaultConfig, but was %t", keyConfig.Config)
}

cfg.Token, err = r.GetSecret(cfg.Token)
if err != nil {
return err
}

sp, err := secrets.NewVaultSecretProviderFromConfig(cfg)
if err != nil {
return err
}

r.keyProvider[keyConfig.Kind] = sp
r.keyProvider["default"] = sp
}
}

return nil
}

func (r *Registry) configureSecrets(config Config) error {
if r.secrets == nil {
r.secrets = map[string]secrets.SecretStorage{}
Expand Down
Loading

0 comments on commit a024207

Please sign in to comment.