Repository: google/exposure-notifications-server
Pull request #492 (2020-05-29 21:56:54)

Support runtime configuration of all current components
This resolves most TODOs in setup by resolving runtime configuration values for:

- Key Management
- Secret Management
- Storage (already done, but refactored a bit)
- Observability (already done, but tuned a bit)

And documents it 😄.

google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/492#" title="Author self-approved">sethvargo</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [sethvargo]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
sethvargo: /assign @mikehelmick 
mikehelmick: /lgtm
/approve
google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/492#issuecomment-636250536" title="Approved">mikehelmick</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/492#" title="Author self-approved">sethvargo</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [mikehelmick,sethvargo]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/docs/deploying.md b/docs/deploying.md
index 4d01f3e..93e7b9c 100644
--- a/docs/deploying.md
+++ b/docs/deploying.md
@@ -11,6 +11,11 @@
   - [On Google Cloud](#on-google-cloud)
   - [On a custom setup](#on-a-custom-setup)
 - [Configuring the server](#configuring-the-server)
+- [Configuring components](#configuring-components)
+  - [Blob storage](#blob-storage)
+  - [Key management](#key-management)
+  - [Secrets management](#secrets-management)
+  - [Observability](#observability)
 
 <!-- /TOC -->
 
@@ -262,3 +267,62 @@ the public Internet!**
 1.  Open a browser to [localhost:8080](http://localhost:8080/).
 
     **Remember, you are editing the live configuration of the database!**
+
+
+## Configuring components
+
+### Blob storage
+
+The blob storage component defines where and how export files are written. The
+following configurations are available:
+
+| Name                   | `BLOBSTORE` value      | Description
+| ---------------------- | ---------------------- | -----------
+| Google Cloud Storage\* | `GOOGLE_CLOUD_STORAGE` | Store data in Google Cloud Storage.
+| Filesystem             | `FILESYSTEM`           | Store data on a filesystem.
+| Noop                   | `NOOP`                 | No files are written.
+
+\* default
+
+### Key management
+
+The key management component is responsible for signing exports. The following
+configurations are available:
+
+| Name               | `KEY_MANAGER` value | Description
+| ------------------ | ------------------- | -----------
+| Google Cloud KMS\* | `GOOGLE_CLOUD_KMS`  | Perform signing using Google Cloud KMS.
+| HashiCorp Vault    | `HASHICORP_VAULT`   | Perform signing using HashiCorp Vault.
+
+\* default
+
+
+### Secrets management
+
+The secrets management component is responsible for acquiring secrets. The
+following configurations are available:
+
+| Name                    | `SECRET_MANAGER` value  | Description
+| ----------------------- | ----------------------- | -----------
+| Azure Key Vault         | `AZURE_KEY_VAULT`       | Resolve with Key Vault.
+| Google Secret Manager\* | `GOOGLE_SECRET_MANAGER` | Resolve with Secret Manager.
+| HashiCorp Vault         | `HASHICORP_VAULT`       | Resolve with Vault.
+
+\* default
+
+In the interest of performance, secrets are TTL-cached to eliminate the secret
+manager from most hot paths. Configure this TTL with `SECRET_CACHE_TTL` which is
+expressed as a time duration like "5m" or "15s". The default cache time is 5
+minutes and lower values are strongly discouraged.
+
+### Observability
+
+The observability component is responsible for metrics. The following
+configurations are available:
+
+| Name                    | `OBSERVABILITY_EXPORTER` value  | Description
+| ----------------------- | ------------------------------- | -----------
+| OpenCensus Agent        | `OCAGENT`                       | Use OpenCensus.
+| Stackdriver\*           | `STACKDRIVER`                   | Use Stackdriver.
+
+\* default
diff --git a/internal/admin/config.go b/internal/admin/config.go
index 6c4e8e9..cd390cb 100644
--- a/internal/admin/config.go
+++ b/internal/admin/config.go
@@ -23,24 +23,45 @@ import (
 	"net/http"
 
 	"github.com/google/exposure-notifications-server/internal/database"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
+	"github.com/google/exposure-notifications-server/internal/signing"
+	"github.com/google/exposure-notifications-server/internal/storage"
 )
 
-var _ setup.DBConfigProvider = (*Config)(nil)
+var _ setup.DatabaseConfigProvider = (*Config)(nil)
+var _ setup.KeyManagerConfigProvider = (*Config)(nil)
+var _ setup.SecretManagerConfigProvider = (*Config)(nil)
+var _ setup.BlobstoreConfigProvider = (*Config)(nil)
 
 type Config struct {
+	Database      *database.Config
+	KeyManager    *signing.Config
+	SecretManager *secrets.Config
+	Storage       *storage.Config
+
 	Port         string `envconfig:"PORT" default:"8080"`
 	TemplatePath string `envconfig:"TEMPLATE_DIR" default:"./tools/admin-console/templates"`
-	Database     *database.Config
 	TopFile      string `envconfig:"TOP_FILE" default:"top"`
 	BotFile      string `envconfig:"BOTTOM_FILE" default:"bottom"`
 }
 
-// DB returns the configuration for the databse.
-func (c *Config) DB() *database.Config {
+func (c *Config) DatabaseConfig() *database.Config {
 	return c.Database
 }
 
+func (c *Config) KeyManagerConfig() *signing.Config {
+	return c.KeyManager
+}
+
+func (c *Config) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
+}
+
+func (c *Config) BlobstoreConfig() *storage.Config {
+	return c.Storage
+}
+
 func (c *Config) RenderTemplate(w http.ResponseWriter, tmpl string, p TemplateMap) error {
 	files := []string{
 		fmt.Sprintf("%s/%s.html", c.TemplatePath, c.TopFile),
diff --git a/internal/cleanup/config.go b/internal/cleanup/config.go
index 9de7908..68ced8e 100644
--- a/internal/cleanup/config.go
+++ b/internal/cleanup/config.go
@@ -18,32 +18,36 @@ import (
 	"time"
 
 	"github.com/google/exposure-notifications-server/internal/database"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
 	"github.com/google/exposure-notifications-server/internal/storage"
 )
 
 // Compile-time check to assert this config matches requirements.
-var _ setup.BlobStorageConfigProvider = (*Config)(nil)
-var _ setup.DBConfigProvider = (*Config)(nil)
+var _ setup.BlobstoreConfigProvider = (*Config)(nil)
+var _ setup.DatabaseConfigProvider = (*Config)(nil)
+var _ setup.SecretManagerConfigProvider = (*Config)(nil)
 
 // Config represents the configuration and associated environment variables for
 // the cleanup components.
 type Config struct {
-	Port          string        `envconfig:"PORT" default:"8080"`
-	Timeout       time.Duration `envconfig:"CLEANUP_TIMEOUT" default:"10m"`
-	TTL           time.Duration `envconfig:"CLEANUP_TTL" default:"336h"`
+	Storage       *storage.Config
 	Database      *database.Config
-	BlobstoreType string `envconfig:"BLOBSTORE_TYPE" default:"CLOUD_STORAGE"`
+	SecretManager *secrets.Config
+
+	Port    string        `envconfig:"PORT" default:"8080"`
+	Timeout time.Duration `envconfig:"CLEANUP_TIMEOUT" default:"10m"`
+	TTL     time.Duration `envconfig:"CLEANUP_TTL" default:"336h"`
+}
+
+func (c *Config) BlobstoreConfig() *storage.Config {
+	return c.Storage
 }
 
-// DB return the databsae configuration.
-func (c *Config) DB() *database.Config {
+func (c *Config) DatabaseConfig() *database.Config {
 	return c.Database
 }
 
-// BlobStorage returns the BlobStorage configuration.
-func (c *Config) BlobStorage() storage.BlobstoreConfig {
-	return storage.BlobstoreConfig{
-		BlobstoreType: storage.BlobstoreType(c.BlobstoreType),
-	}
+func (c *Config) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
 }
diff --git a/internal/export/config.go b/internal/export/config.go
index 9b36081..955b38c 100644
--- a/internal/export/config.go
+++ b/internal/export/config.go
@@ -18,19 +18,26 @@ import (
 	"time"
 
 	"github.com/google/exposure-notifications-server/internal/database"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
+	"github.com/google/exposure-notifications-server/internal/signing"
 	"github.com/google/exposure-notifications-server/internal/storage"
 )
 
-// Compile-time check to assert this config matches requirements.
-var _ setup.KeyManagerProvider = (*Config)(nil)
-var _ setup.BlobStorageConfigProvider = (*Config)(nil)
-var _ setup.DBConfigProvider = (*Config)(nil)
+// Compile-time check to assert this config matches requirements
+var _ setup.BlobstoreConfigProvider = (*Config)(nil)
+var _ setup.DatabaseConfigProvider = (*Config)(nil)
+var _ setup.KeyManagerConfigProvider = (*Config)(nil)
+var _ setup.SecretManagerConfigProvider = (*Config)(nil)
 
 // Config represents the configuration and associated environment variables for
 // the export components.
 type Config struct {
-	Database       *database.Config
+	Storage       *storage.Config
+	Database      *database.Config
+	KeyManager    *signing.Config
+	SecretManager *secrets.Config
+
 	Port           string        `envconfig:"PORT" default:"8080"`
 	CreateTimeout  time.Duration `envconfig:"CREATE_BATCHES_TIMEOUT" default:"5m"`
 	WorkerTimeout  time.Duration `envconfig:"WORKER_TIMEOUT" default:"5m"`
@@ -39,23 +46,21 @@ type Config struct {
 	MaxRecords     int           `envconfig:"EXPORT_FILE_MAX_RECORDS" default:"30000"`
 	TruncateWindow time.Duration `envconfig:"TRUNCATE_WINDOW" default:"1h"`
 	MinWindowAge   time.Duration `envconfig:"MIN_WINDOW_AGE" default:"2h"`
-	BlobstoreType  string        `envconfig:"BLOBSTORE_TYPE" default:"CLOUD_STORAGE"`
 	TTL            time.Duration `envconfig:"CLEANUP_TTL" default:"336h"`
 }
 
-// DB returns the database config.
-func (c *Config) DB() *database.Config {
+func (c *Config) BlobstoreConfig() *storage.Config {
+	return c.Storage
+}
+
+func (c *Config) DatabaseConfig() *database.Config {
 	return c.Database
 }
 
-// KeyManager returns the KeyManager configuration.
-func (c *Config) KeyManager() bool {
-	return true
+func (c *Config) KeyManagerConfig() *signing.Config {
+	return c.KeyManager
 }
 
-// BlobStorage returns the BlobStorage configuration.
-func (c *Config) BlobStorage() storage.BlobstoreConfig {
-	return storage.BlobstoreConfig{
-		BlobstoreType: storage.BlobstoreType(c.BlobstoreType),
-	}
+func (c *Config) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
 }
diff --git a/internal/federationin/config.go b/internal/federationin/config.go
index c0d4e85..ac9c900 100644
--- a/internal/federationin/config.go
+++ b/internal/federationin/config.go
@@ -19,6 +19,7 @@ import (
 	"time"
 
 	"github.com/google/exposure-notifications-server/internal/database"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
 )
 
@@ -35,11 +36,14 @@ var (
 )
 
 // Compile-time check to assert this config matches requirements.
-var _ setup.DBConfigProvider = (*Config)(nil)
+var _ setup.DatabaseConfigProvider = (*Config)(nil)
+var _ setup.SecretManagerConfigProvider = (*Config)(nil)
 
 // Config is the configuration for federation-pull components (data pulled from other servers).
 type Config struct {
-	Database       *database.Config
+	Database      *database.Config
+	SecretManager *secrets.Config
+
 	Port           string        `envconfig:"PORT" default:"8080"`
 	Timeout        time.Duration `envconfig:"RPC_TIMEOUT" default:"10m"`
 	TruncateWindow time.Duration `envconfig:"TRUNCATE_WINDOW" default:"1h"`
@@ -56,7 +60,10 @@ type Config struct {
 	CredentialsFile string `envconfig:"CREDENTIALS_FILE"`
 }
 
-// DB returns the database config.
-func (c *Config) DB() *database.Config {
+func (c *Config) DatabaseConfig() *database.Config {
 	return c.Database
 }
+
+func (c *Config) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
+}
diff --git a/internal/federationout/config.go b/internal/federationout/config.go
index 98b9b7e..0cfeb67 100644
--- a/internal/federationout/config.go
+++ b/internal/federationout/config.go
@@ -18,15 +18,19 @@ import (
 	"time"
 
 	"github.com/google/exposure-notifications-server/internal/database"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
 )
 
 // Compile-time check to assert this config matches requirements.
-var _ setup.DBConfigProvider = (*Config)(nil)
+var _ setup.DatabaseConfigProvider = (*Config)(nil)
+var _ setup.SecretManagerConfigProvider = (*Config)(nil)
 
 // Config is the configuration for the federation components (data sent to other servers).
 type Config struct {
-	Database       *database.Config
+	Database      *database.Config
+	SecretManager *secrets.Config
+
 	Port           string        `envconfig:"PORT" default:"8080"`
 	Timeout        time.Duration `envconfig:"RPC_TIMEOUT" default:"5m"`
 	TruncateWindow time.Duration `envconfig:"TRUNCATE_WINDOW" default:"1h"`
@@ -42,7 +46,10 @@ type Config struct {
 	TLSKeyFile  string `envconfig:"TLS_KEY_FILE"`
 }
 
-// DB returns the database config.
-func (c *Config) DB() *database.Config {
+func (c *Config) DatabaseConfig() *database.Config {
 	return c.Database
 }
+
+func (c *Config) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
+}
diff --git a/internal/generate/config.go b/internal/generate/config.go
index c0ff45a..45da6c5 100644
--- a/internal/generate/config.go
+++ b/internal/generate/config.go
@@ -19,15 +19,20 @@ import (
 	"time"
 
 	"github.com/google/exposure-notifications-server/internal/database"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
 )
 
 // Compile-time check to assert this config matches requirements.
-var _ setup.DBConfigProvider = (*Config)(nil)
+var _ setup.DatabaseConfigProvider = (*Config)(nil)
+var _ setup.SecretManagerConfigProvider = (*Config)(nil)
 
 // Config represents the configuration and associated environment variables for
 // the publish components.
 type Config struct {
+	Database      *database.Config
+	SecretManager *secrets.Config
+
 	Port             string        `envconfig:"PORT" default:"8080"`
 	NumExposures     int           `envconfig:"NUM_EXPOSURES_GENERATED" default:"10"`
 	KeysPerExposure  int           `envconfig:"KEYS_PER_EXPOSURE" default:"14"`
@@ -35,11 +40,12 @@ type Config struct {
 	MaxIntervalAge   time.Duration `envconfig:"MAX_INTERVAL_AGE_ON_PUBLISH" default:"360h"`
 	TruncateWindow   time.Duration `envconfig:"TRUNCATE_WINDOW" default:"1h"`
 	DefaultRegion    string        `envconfig:"DEFAULT_REGOIN" default:"US"`
-
-	Database *database.Config
 }
 
-// DB returns the configuration for the databse.
-func (c *Config) DB() *database.Config {
+func (c *Config) DatabaseConfig() *database.Config {
 	return c.Database
 }
+
+func (c *Config) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
+}
diff --git a/internal/monolith/monolith_util.go b/internal/monolith/monolith_util.go
index f0c39cb..86b704a 100644
--- a/internal/monolith/monolith_util.go
+++ b/internal/monolith/monolith_util.go
@@ -30,41 +30,56 @@ import (
 	"github.com/google/exposure-notifications-server/internal/handlers"
 	"github.com/google/exposure-notifications-server/internal/logging"
 	"github.com/google/exposure-notifications-server/internal/publish"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
+	"github.com/google/exposure-notifications-server/internal/signing"
 	"github.com/google/exposure-notifications-server/internal/storage"
 
 	// Enable observability with distributed tracing and metrics.
 	_ "github.com/google/exposure-notifications-server/internal/observability"
 )
 
-var _ setup.DBConfigProvider = (*MonoConfig)(nil)
 var _ setup.AuthorizedAppConfigProvider = (*MonoConfig)(nil)
-var _ setup.BlobStorageConfigProvider = (*MonoConfig)(nil)
-var _ setup.KeyManagerProvider = (*MonoConfig)(nil)
+var _ setup.BlobstoreConfigProvider = (*MonoConfig)(nil)
+var _ setup.DatabaseConfigProvider = (*MonoConfig)(nil)
+var _ setup.KeyManagerConfigProvider = (*MonoConfig)(nil)
+var _ setup.SecretManagerConfigProvider = (*MonoConfig)(nil)
 
 type MonoConfig struct {
-	Port string `envconfig:"PORT" default:"8080"`
-
 	AuthorizedApp *authorizedapp.Config
+	Storage       *storage.Config
 	Cleanup       *cleanup.Config
-	Export        *export.Config
-	Publish       *publish.Config
 	Database      *database.Config
+	Export        *export.Config
 	FederationIn  *federationin.Config
-	BlobstoreType string `envconfig:"BLOBSTORE_TYPE" default:"CLOUD_STORAGE"`
+	KeyManager    *signing.Config
+	Publish       *publish.Config
+	SecretManager *secrets.Config
+
+	Port string `envconfig:"PORT" default:"8080"`
 }
 
-func (c *MonoConfig) DB() *database.Config { return c.Database }
-func (c *MonoConfig) KeyManager() bool     { return true }
-func (c *MonoConfig) BlobStorage() storage.BlobstoreConfig {
-	return storage.BlobstoreConfig{
-		BlobstoreType: storage.BlobstoreType(c.BlobstoreType),
-	}
+func (c *MonoConfig) AuthorizedAppConfig() *authorizedapp.Config {
+	return c.AuthorizedApp
 }
-func (c *MonoConfig) AuthorizedAppConfig() *authorizedapp.Config { return c.AuthorizedApp }
 
-func RunServer(ctx context.Context) (*MonoConfig, error) {
+func (c *MonoConfig) BlobstoreConfig() *storage.Config {
+	return c.Storage
+}
 
+func (c *MonoConfig) DatabaseConfig() *database.Config {
+	return c.Database
+}
+
+func (c *MonoConfig) KeyManagerConfig() *signing.Config {
+	return c.KeyManager
+}
+
+func (c *MonoConfig) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
+}
+
+func RunServer(ctx context.Context) (*MonoConfig, error) {
 	logger := logging.FromContext(ctx)
 
 	var config MonoConfig
diff --git a/internal/observability/observability.go b/internal/observability/observability.go
index e04e8d7..3e1cca3 100644
--- a/internal/observability/observability.go
+++ b/internal/observability/observability.go
@@ -16,6 +16,7 @@ package observability
 
 import (
 	"os"
+	"strings"
 	"sync"
 
 	"contrib.go.opencensus.io/exporter/ocagent"
@@ -64,19 +65,19 @@ type traceAndViewExporter interface {
 }
 
 func exporter() traceAndViewExporter {
-	switch os.Getenv("OBSERVABILITY_EXPORTER") {
+	switch strings.ToUpper(os.Getenv("OBSERVABILITY_EXPORTER")) {
 	default:
 		// TODO: Add other trace and view exporters and break out of this.
 		return nil
 
-	case "ocagent":
+	case "OCAGENT":
 		oce, err := ocagent.NewExporter(ocagent.WithInsecure(), ocagent.WithAddress("localhost:55678"))
 		if err != nil {
 			panic(err)
 		}
 		return oce
 
-	case "stackdriver":
+	case "STACKDRIVER":
 		sde, err := stackdriver.NewExporter(stackdriver.Options{
 			ProjectID: os.Getenv("PROJECT_ID"),
 		})
diff --git a/internal/publish/config.go b/internal/publish/config.go
index f565930..73f25c5 100644
--- a/internal/publish/config.go
+++ b/internal/publish/config.go
@@ -20,16 +20,22 @@ import (
 
 	"github.com/google/exposure-notifications-server/internal/authorizedapp"
 	"github.com/google/exposure-notifications-server/internal/database"
+	"github.com/google/exposure-notifications-server/internal/secrets"
 	"github.com/google/exposure-notifications-server/internal/setup"
 )
 
 // Compile-time check to assert this config matches requirements.
 var _ setup.AuthorizedAppConfigProvider = (*Config)(nil)
-var _ setup.DBConfigProvider = (*Config)(nil)
+var _ setup.DatabaseConfigProvider = (*Config)(nil)
+var _ setup.SecretManagerConfigProvider = (*Config)(nil)
 
 // Config represents the configuration and associated environment variables for
 // the publish components.
 type Config struct {
+	AuthorizedApp *authorizedapp.Config
+	Database      *database.Config
+	SecretManager *secrets.Config
+
 	Port               string        `envconfig:"PORT" default:"8080"`
 	MinRequestDuration time.Duration `envconfig:"TARGET_REQUEST_DURATION" default:"5s"`
 	MaxKeysOnPublish   int           `envconfig:"MAX_KEYS_ON_PUBLISH" default:"15"`
@@ -39,17 +45,16 @@ type Config struct {
 	// Flags for local development and testing.
 	DebugAPIResponses   bool `envconfig:"DEBUG_API_RESPONSES"`
 	DebugAllowRestOfDay bool `envconfig:"DEBUG_ALLOW_REST_OF_DAY"`
-
-	AuthorizedApp *authorizedapp.Config
-	Database      *database.Config
 }
 
-// AuthorizedApp returns the configuration for authorizedapp.
 func (c *Config) AuthorizedAppConfig() *authorizedapp.Config {
 	return c.AuthorizedApp
 }
 
-// DB returns the configuration for the databse.
-func (c *Config) DB() *database.Config {
+func (c *Config) DatabaseConfig() *database.Config {
 	return c.Database
 }
+
+func (c *Config) SecretManagerConfig() *secrets.Config {
+	return c.SecretManager
+}
diff --git a/internal/secrets/cacher.go b/internal/secrets/cacher.go
index eec019d..b5d18f8 100644
--- a/internal/secrets/cacher.go
+++ b/internal/secrets/cacher.go
@@ -48,16 +48,16 @@ func NewCacher(ctx context.Context, f SecretManagerFunc, ttl time.Duration) (Sec
 		return nil, fmt.Errorf("cacher: %w", err)
 	}
 
-	return WrapCacher(ctx, sm, ttl)
+	return WrapCacher(ctx, sm, ttl), nil
 }
 
 // WrapCacher wraps an existing SecretManager with caching.
-func WrapCacher(ctx context.Context, sm SecretManager, ttl time.Duration) (SecretManager, error) {
+func WrapCacher(ctx context.Context, sm SecretManager, ttl time.Duration) SecretManager {
 	return &Cacher{
 		sm:    sm,
 		ttl:   ttl,
 		cache: make(map[string]*cachedItem),
-	}, nil
+	}
 }
 
 // GetSecretValue implements the SecretManager interface, but caches values and
diff --git a/internal/secrets/gcp_secret_manager.go b/internal/secrets/google_secret_manager.go
similarity index 78%
rename from internal/secrets/gcp_secret_manager.go
rename to internal/secrets/google_secret_manager.go
index 2e969a6..2307b37 100644
--- a/internal/secrets/gcp_secret_manager.go
+++ b/internal/secrets/google_secret_manager.go
@@ -23,21 +23,21 @@ import (
 )
 
 // Compile-time check to verify implements interface.
-var _ SecretManager = (*GCPSecretManager)(nil)
+var _ SecretManager = (*GoogleSecretManager)(nil)
 
-// GCPSecretManager implements SecretManager.
-type GCPSecretManager struct {
+// GoogleSecretManager implements SecretManager.
+type GoogleSecretManager struct {
 	client *secretmanager.Client
 }
 
-// NewGCPSecretManager creates a new secret manager for GCP.
-func NewGCPSecretManager(ctx context.Context) (SecretManager, error) {
+// NewGoogleSecretManager creates a new secret manager for GCP.
+func NewGoogleSecretManager(ctx context.Context) (SecretManager, error) {
 	client, err := secretmanager.NewClient(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("secretmanager.NewClient: %w", err)
 	}
 
-	sm := &GCPSecretManager{
+	sm := &GoogleSecretManager{
 		client: client,
 	}
 
@@ -48,7 +48,7 @@ func NewGCPSecretManager(ctx context.Context) (SecretManager, error) {
 // of the format:
 //
 //     projects/my-project/secrets/my-secret/versions/123
-func (sm *GCPSecretManager) GetSecretValue(ctx context.Context, name string) (string, error) {
+func (sm *GoogleSecretManager) GetSecretValue(ctx context.Context, name string) (string, error) {
 	result, err := sm.client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
 		Name: name,
 	})
diff --git a/internal/secrets/secrets.go b/internal/secrets/secrets.go
index 8ea4d51..ff74e3d 100644
--- a/internal/secrets/secrets.go
+++ b/internal/secrets/secrets.go
@@ -16,7 +16,26 @@
 // Allows for a different implementation to be bound within the servernv.ServeEnv
 package secrets
 
-import "context"
+import (
+	"context"
+	"fmt"
+	"time"
+)
+
+// SecretManagerType represents a type of secret manager.
+type SecretManagerType string
+
+const (
+	SecretManagerTypeAzureKeyVault        SecretManagerType = "AZURE_KEY_VAULT"
+	SecretManagerTypeGoogleSecretManager  SecretManagerType = "GOOGLE_SECRET_MANAGER"
+	SecretManagerTypeGoogleHashiCorpVault SecretManagerType = "HASHICORP_VAULT"
+)
+
+// Config represents the config for a secret manager.
+type Config struct {
+	SecretManagerType SecretManagerType `envconfig:"SECRET_MANAGER" default:"GOOGLE_SECRET_MANAGER"`
+	SecretCacheTTL    time.Duration     `envconfig:"SECRET_CACHE_TTL" default:"5m"`
+}
 
 // SecretManager defines the minimum shared functionality for a secret manager
 // used by this application.
@@ -26,3 +45,18 @@ type SecretManager interface {
 
 // SecretManagerFunc is a func that returns a secret manager or error.
 type SecretManagerFunc func(ctx context.Context) (SecretManager, error)
+
+// SecretManagerFor returns the secret manager for the given type, or an error
+// if one does not exist.
+func SecretManagerFor(ctx context.Context, typ SecretManagerType) (SecretManager, error) {
+	switch typ {
+	case SecretManagerTypeAzureKeyVault:
+		return NewAzureKeyVault(ctx)
+	case SecretManagerTypeGoogleSecretManager:
+		return NewGoogleSecretManager(ctx)
+	case SecretManagerTypeGoogleHashiCorpVault:
+		return NewHashiCorpVault(ctx)
+	}
+
+	return nil, fmt.Errorf("unknown secret manager type: %v", typ)
+}
diff --git a/internal/setup/setup.go b/internal/setup/setup.go
index 82b1040..7e7e859 100644
--- a/internal/setup/setup.go
+++ b/internal/setup/setup.go
@@ -17,7 +17,6 @@ package setup
 import (
 	"context"
 	"fmt"
-	"time"
 
 	"github.com/google/exposure-notifications-server/internal/authorizedapp"
 	"github.com/google/exposure-notifications-server/internal/database"
@@ -30,10 +29,16 @@ import (
 	"github.com/google/exposure-notifications-server/internal/storage"
 )
 
-// DBConfigProvider ensures that the environment config can provide a DB config.
+// BlobstoreConfigProvider provides the information about current storage
+// configuration.
+type BlobstoreConfigProvider interface {
+	BlobstoreConfig() *storage.Config
+}
+
+// DatabaseConfigProvider ensures that the environment config can provide a DB config.
 // All binaries in this application connect to the database via the same method.
-type DBConfigProvider interface {
-	DB() *database.Config
+type DatabaseConfigProvider interface {
+	DatabaseConfig() *database.Config
 }
 
 // AuthorizedAppConfigProvider signals that the config provided knows how to
@@ -42,31 +47,44 @@ type AuthorizedAppConfigProvider interface {
 	AuthorizedAppConfig() *authorizedapp.Config
 }
 
-// KeyManagerProvider is a marker interface indicating the KeyManagerProvider should be installed.
-type KeyManagerProvider interface {
-	KeyManager() bool
+// KeyManagerConfigProvider is a marker interface indicating the key manager
+// should be installed.
+type KeyManagerConfigProvider interface {
+	KeyManagerConfig() *signing.Config
 }
 
-// BlobStorageConfigProvider provides the information about current Blobstore configuration.
-type BlobStorageConfigProvider interface {
-	BlobStorage() storage.BlobstoreConfig
+// SecretManagerConfigProvider signals that the config knows how to configure a
+// secret manager.
+type SecretManagerConfigProvider interface {
+	SecretManagerConfig() *secrets.Config
 }
 
 // Function returned from setup to be deferred until the caller exits.
 type Defer func()
 
 // Setup runs common initialization code for all servers.
-func Setup(ctx context.Context, config DBConfigProvider) (*serverenv.ServerEnv, Defer, error) {
+func Setup(ctx context.Context, config DatabaseConfigProvider) (*serverenv.ServerEnv, Defer, error) {
 	logger := logging.FromContext(ctx)
 
-	// Can be changed with a different secret manager interface.
-	// TODO(mikehelmick): Make this extensible to other providers.
-	// TODO(sethvargo): Make TTL configurable.
-	sm, err := secrets.NewCacher(ctx, secrets.NewGCPSecretManager, 5*time.Minute)
-	if err != nil {
-		return nil, nil, fmt.Errorf("unable to connect to secret manager: %v", err)
+	// Load the secret manager - this needs to be loaded first because other
+	// processors may require access to secrets.
+	var sm secrets.SecretManager
+	if provider, ok := config.(SecretManagerConfigProvider); ok {
+		smConfig := provider.SecretManagerConfig()
+
+		var err error
+		sm, err = secrets.SecretManagerFor(ctx, smConfig.SecretManagerType)
+		if err != nil {
+			return nil, nil, fmt.Errorf("unable to connect to secret manager: %v", err)
+		}
+
+		// Enable caching, if provided.
+		if ttl := smConfig.SecretCacheTTL; ttl > 0 {
+			sm = secrets.WrapCacher(ctx, sm, ttl)
+		}
 	}
 
+	// Process first round of environment variables.
 	if err := envconfig.Process(ctx, config, sm); err != nil {
 		return nil, nil, fmt.Errorf("error loading environment variables: %v", err)
 	}
@@ -78,18 +96,20 @@ func Setup(ctx context.Context, config DBConfigProvider) (*serverenv.ServerEnv,
 		serverenv.WithMetricsExporter(metrics.NewLogsBasedFromContext),
 	}
 
-	// TODO(mikehelmick): Make this extensible to other providers.
-	if _, ok := config.(KeyManagerProvider); ok {
-		km, err := signing.NewGCPKMS(ctx)
+	// Configure key management for signing.
+	if provider, ok := config.(KeyManagerConfigProvider); ok {
+		kmConfig := provider.KeyManagerConfig()
+		km, err := signing.KeyManagerFor(ctx, kmConfig.KeyManagerType)
 		if err != nil {
 			return nil, nil, fmt.Errorf("unable to connect to key manager: %w", err)
 		}
 		opts = append(opts, serverenv.WithKeyManager(km))
 	}
 
-	if _, ok := config.(BlobStorageConfigProvider); ok {
-		provider := config.(BlobStorageConfigProvider)
-		blobStore, err := storage.CreateBlobstore(ctx, provider.BlobStorage())
+	// Configure blob storage.
+	if provider, ok := config.(BlobstoreConfigProvider); ok {
+		bsConfig := provider.BlobstoreConfig()
+		blobStore, err := storage.BlobstoreFor(ctx, bsConfig.BlobstoreType)
 		if err != nil {
 			return nil, nil, fmt.Errorf("unable to connect to storage system: %v", err)
 		}
@@ -98,13 +118,13 @@ func Setup(ctx context.Context, config DBConfigProvider) (*serverenv.ServerEnv,
 	}
 
 	// Setup the database connection.
-	db, err := database.NewFromEnv(ctx, config.DB())
+	db, err := database.NewFromEnv(ctx, config.DatabaseConfig())
 	if err != nil {
 		return nil, nil, fmt.Errorf("unable to connect to database: %v", err)
 	}
 	{
 		// Log the database config, but omit the password field.
-		redactedDB := config.DB()
+		redactedDB := config.DatabaseConfig()
 		redactedDB.Password = "<hidden>"
 		logger.Infof("Effective DB config: %+v", redactedDB)
 	}
diff --git a/internal/signing/gcpkms.go b/internal/signing/google_cloud_kms.go
similarity index 74%
rename from internal/signing/gcpkms.go
rename to internal/signing/google_cloud_kms.go
index a300879..643df42 100644
--- a/internal/signing/gcpkms.go
+++ b/internal/signing/google_cloud_kms.go
@@ -23,23 +23,23 @@ import (
 )
 
 // Compile-time check to verify implements interface.
-var _ KeyManager = (*GCPKMS)(nil)
+var _ KeyManager = (*GoogleCloudKMS)(nil)
 
-// GCPKMS implements the signing.KeyManager interface and can be used to sign
+// GoogleCloudKMS implements the signing.KeyManager interface and can be used to sign
 // export files.
-type GCPKMS struct {
+type GoogleCloudKMS struct {
 	client *kms.KeyManagementClient
 }
 
-func NewGCPKMS(ctx context.Context) (KeyManager, error) {
+func NewGoogleCloudKMS(ctx context.Context) (KeyManager, error) {
 	client, err := kms.NewKeyManagementClient(ctx)
 	if err != nil {
 		return nil, err
 	}
-	return &GCPKMS{client}, nil
+	return &GoogleCloudKMS{client}, nil
 }
 
-func (kms *GCPKMS) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
+func (kms *GoogleCloudKMS) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
 	signer, err := gcpkms.NewSigner(ctx, kms.client, keyID)
 	if err != nil {
 		return nil, err
diff --git a/internal/signing/signing.go b/internal/signing/signing.go
index eaca638..ab106c4 100644
--- a/internal/signing/signing.go
+++ b/internal/signing/signing.go
@@ -18,11 +18,37 @@ package signing
 import (
 	"context"
 	"crypto"
+	"fmt"
 )
 
+// KeyManagerType defines a specific key manager.
+type KeyManagerType string
+
+const (
+	KeyManagerTypeGoogleCloudKMS KeyManagerType = "GOOGLE_CLOUD_KMS"
+	KeyManagerTypeHashiCorpVault KeyManagerType = "HASHICORP_VAULT"
+)
+
+// Config defines configuration.
+type Config struct {
+	KeyManagerType KeyManagerType `envconfig:"KEY_MANAGER" default:"GOOGLE_CLOUD_KMS"`
+}
+
 // KeyManager defines the interface for working with a KMS system that
 // is able to sign bytes using PKI.
 // KeyManager implementations must be able to return a crypto.Signer
 type KeyManager interface {
 	NewSigner(ctx context.Context, keyID string) (crypto.Signer, error)
 }
+
+// KeyManagerFor returns the appropriate key manager for the given type.
+func KeyManagerFor(ctx context.Context, typ KeyManagerType) (KeyManager, error) {
+	switch typ {
+	case KeyManagerTypeGoogleCloudKMS:
+		return NewGoogleCloudKMS(ctx)
+	case KeyManagerTypeHashiCorpVault:
+		return NewHashiCorpVault(ctx)
+	}
+
+	return nil, fmt.Errorf("unknown key manager type: %v", typ)
+}
diff --git a/internal/storage/noop_storage.go b/internal/storage/noop_storage.go
new file mode 100644
index 0000000..75a2340
--- /dev/null
+++ b/internal/storage/noop_storage.go
@@ -0,0 +1,35 @@
+// Copyright 2020 Google LLC
+//
+// 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 storage
+
+import "context"
+
+// Compile-time check to verify implements.
+var _ Blobstore = (*NoopBlobstore)(nil)
+
+// NoopBlobstore is a blobstore that does nothing.
+type NoopBlobstore struct{}
+
+func NewNoopBlobstore(ctx context.Context) (Blobstore, error) {
+	return &NoopBlobstore{}, nil
+}
+
+func (s *NoopBlobstore) CreateObject(ctx context.Context, folder, filename string, contents []byte) error {
+	return nil
+}
+
+func (s *NoopBlobstore) DeleteObject(ctx context.Context, folder, filename string) error {
+	return nil
+}
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index 1e8769f..c95f097 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -18,22 +18,20 @@ package storage
 import (
 	"context"
 	"fmt"
-
-	"github.com/google/exposure-notifications-server/internal/logging"
 )
 
-// Identifies the type of Blobestore to use
+// BlobstoreType defines a specific blobstore.
 type BlobstoreType string
 
 const (
-	None          BlobstoreType = "NONE"
-	Cloud_storage BlobstoreType = "CLOUD_STORAGE"
-	Filesystem    BlobstoreType = "FILESYSTEM"
+	BlobstoreTypeGoogleCloudStorage BlobstoreType = "GOOGLE_CLOUD_STORAGE"
+	BlobstoreTypeFilesystem         BlobstoreType = "FILESYSTEM"
+	BlobstoreTypeNoop               BlobstoreType = "NOOP"
 )
 
-// Blobstore Configuration
-type BlobstoreConfig struct {
-	BlobstoreType BlobstoreType
+// Config defines the configuration for a blobstore.
+type Config struct {
+	BlobstoreType BlobstoreType `envconfig:"BLOBSTORE" default:"GOOGLE_CLOUD_STORAGE"`
 }
 
 // Blobstore defines the minimum interface for a blob storage system.
@@ -45,36 +43,17 @@ type Blobstore interface {
 	DeleteObject(ctx context.Context, bucket, objectName string) error
 }
 
-// Blobstore that does nothing.
-type NoopBlobstore struct{}
-
-func NewNoopBlobstore(ctx context.Context) (Blobstore, error) {
-	return &NoopBlobstore{}, nil
-}
-
-// No op.
-func (s *NoopBlobstore) CreateObject(ctx context.Context, folder, filename string, contents []byte) error {
-	return nil
-}
-
-// No op.
-func (s *NoopBlobstore) DeleteObject(ctx context.Context, folder, filename string) error {
-	return nil
-}
-
-// Creates a new BlobstoreFactory based on the provided BlobstoreType.
-func CreateBlobstore(ctx context.Context, config BlobstoreConfig) (Blobstore, error) {
-	logger := logging.FromContext(ctx)
-	logger.Infof("BlobstoreType is set up to %v", config.BlobstoreType)
-
-	switch config.BlobstoreType {
-	case Cloud_storage:
+// BlobstoreFor returns the blob store for the given type, or an error if one
+// does not exist.
+func BlobstoreFor(ctx context.Context, typ BlobstoreType) (Blobstore, error) {
+	switch typ {
+	case BlobstoreTypeGoogleCloudStorage:
 		return NewGoogleCloudStorage(ctx)
-	case Filesystem:
+	case BlobstoreTypeFilesystem:
 		return NewFilesystemStorage(ctx)
-	case None:
+	case BlobstoreTypeNoop:
 		return NewNoopBlobstore(ctx)
 	default:
-		return nil, fmt.Errorf("unknown BlobstoreType: %v", config.BlobstoreType)
+		return nil, fmt.Errorf("unknown blob store: %v", typ)
 	}
 }


Test patch:
diff --git a/internal/export/server_test.go b/internal/export/server_test.go
index 58e7a9d..63c74ec 100644
--- a/internal/export/server_test.go
+++ b/internal/export/server_test.go
@@ -28,7 +28,7 @@ import (
 // TestNewServer tests NewServer().
 func TestNewServer(t *testing.T) {
 	emptyStorage := &storage.GoogleCloudStorage{}
-	emptyKMS := &signing.GCPKMS{}
+	emptyKMS := &signing.GoogleCloudKMS{}
 	emptyDB := &database.DB{}
 	ctx := context.Background()
 
diff --git a/internal/secrets/cacher_test.go b/internal/secrets/cacher_test.go
index 2b25062..b9c7080 100644
--- a/internal/secrets/cacher_test.go
+++ b/internal/secrets/cacher_test.go
@@ -34,10 +34,7 @@ func TestCacher_GetSecretValue(t *testing.T) {
 	ctx := context.Background()
 
 	sm := &testSecretManager{value: "first"}
-	cached, err := WrapCacher(ctx, sm, 250*time.Millisecond)
-	if err != nil {
-		t.Fatal(err)
-	}
+	cached := WrapCacher(ctx, sm, 250*time.Millisecond)
 
 	// Read the value once, which should cache it.
 	if _, err := cached.GetSecretValue(ctx, "secret"); err != nil {