Repository: google/exposure-notifications-server
Pull request #331 (2020-05-17 19:51:44)

Implement filesystem blobstore

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/331#" 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 

Patch:
diff --git a/cmd/monolith/main.go b/cmd/monolith/main.go
index a75a491..a1e0581 100644
--- a/cmd/monolith/main.go
+++ b/cmd/monolith/main.go
@@ -20,7 +20,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/google/exposure-notifications-server/internal/apiconfig"
+	"github.com/google/exposure-notifications-server/internal/authorizedapp"
 	"github.com/google/exposure-notifications-server/internal/cleanup"
 	"github.com/google/exposure-notifications-server/internal/database"
 	"github.com/google/exposure-notifications-server/internal/export"
@@ -32,25 +32,25 @@ import (
 )
 
 var _ setup.DBConfigProvider = (*MonoConfig)(nil)
-var _ setup.APIConfigProvider = (*MonoConfig)(nil)
+var _ setup.AuthorizedAppConfigProvider = (*MonoConfig)(nil)
 var _ setup.BlobStorageConfigProvider = (*MonoConfig)(nil)
 var _ setup.KeyManagerProvider = (*MonoConfig)(nil)
 
 type MonoConfig struct {
 	Port string `envconfig:"PORT" default:"8080"`
 
-	APIConfig    *apiconfig.Config
-	Cleanup      *cleanup.Config
-	Export       *export.Config
-	Publish      *publish.Config
-	Database     *database.Config
-	FederationIn *federationin.Config
+	AuthorizedApp *authorizedapp.Config
+	Cleanup       *cleanup.Config
+	Export        *export.Config
+	Publish       *publish.Config
+	Database      *database.Config
+	FederationIn  *federationin.Config
 }
 
-func (c *MonoConfig) DB() *database.Config               { return c.Database }
-func (c *MonoConfig) KeyManager() bool                   { return true }
-func (c *MonoConfig) BlobStorage() bool                  { return true }
-func (c *MonoConfig) APIConfigConfig() *apiconfig.Config { return c.APIConfig }
+func (c *MonoConfig) DB() *database.Config                       { return c.Database }
+func (c *MonoConfig) KeyManager() bool                           { return true }
+func (c *MonoConfig) BlobStorage() bool                          { return true }
+func (c *MonoConfig) AuthorizedAppConfig() *authorizedapp.Config { return c.AuthorizedApp }
 
 func main() {
 	ctx := context.Background()
diff --git a/go.mod b/go.mod
index 44715ba..7760f01 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,17 @@ go 1.14
 require (
 	cloud.google.com/go v0.56.0
 	cloud.google.com/go/storage v1.6.0
+	github.com/Azure/azure-sdk-for-go v42.2.0+incompatible
+	github.com/Azure/go-autorest/autorest v0.10.1 // indirect
+	github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 // indirect
+	github.com/Azure/go-autorest/autorest/to v0.3.0 // indirect
+	github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/golang-migrate/migrate/v4 v4.10.0
 	github.com/golang/protobuf v1.4.0
 	github.com/google/go-cmp v0.4.0
 	github.com/google/uuid v1.1.1
+	github.com/hashicorp/vault/api v1.0.4
 	github.com/jackc/pgx/v4 v4.6.0
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/kr/pretty v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index 5f49c20..3ed74f5 100644
--- a/go.sum
+++ b/go.sum
@@ -29,8 +29,38 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0 h1:UDpwYIwla4jHGzZJaEJYx1tOejbgSoNqsAfHAUYe2r8=
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/azure-sdk-for-go v42.2.0+incompatible h1:ezf8BQIvXYn+LSf+rDqOVyRG3bWkf/SXKYFz4zIBX1Q=
+github.com/Azure/azure-sdk-for-go v42.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
+github.com/Azure/go-autorest/autorest v0.10.1 h1:uaB8A32IZU9YKs9v50+/LWIWTDHJk2vlGzbfd7FfESI=
+github.com/Azure/go-autorest/autorest v0.10.1/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
+github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
+github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
+github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
+github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk=
+github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
+github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U=
+github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
+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.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM=
+github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
+github.com/Azure/go-autorest/autorest/to v0.3.0 h1:zebkZaadz7+wIQYgC7GXaz3Wb28yKYfVkkBKwc38VF8=
+github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=
+github.com/Azure/go-autorest/autorest/validation v0.2.0 h1:15vMO4y76dehZSq7pAaOLQxC6dZYsSrj2GQpflyM/L4=
+github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
+github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -43,8 +73,11 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
 github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
@@ -72,6 +105,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dhui/dktest v0.3.2 h1:nZSDcnkpbotzT/nEHNsO+JCKY8i1Qoki1AYOpeLRb6M=
 github.com/dhui/dktest v0.3.2/go.mod h1:l1/ib23a/CmxAe7yixtrYPc8Iy90Zy2udyaHINM5p58=
+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/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -90,16 +125,20 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
 github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@@ -133,6 +172,7 @@ github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -165,10 +205,34 @@ github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
 github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
+github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
+github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE=
+github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8=
+github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
+github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU=
+github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
+github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8=
+github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
@@ -242,8 +306,10 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc=
 github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@@ -252,12 +318,23 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
 github.com/neo4j-drivers/gobolt v1.7.4/go.mod h1:O9AUbip4Dgre+CD3p40dnMD4a4r52QBIfblg5k7CTbE=
 github.com/neo4j/neo4j-go-driver v1.7.4/go.mod h1:aPO0vVr+WnhEJne+FgFjfsjzAnssPFLucHgGZ76Zb/U=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@@ -266,6 +343,8 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ
 github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
 github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -273,6 +352,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -288,6 +368,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
 github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
+github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sethvargo/go-gcpkms v0.0.0-20200417004547-e50d0c7083d9 h1:RhVXqKbTmy4akECFXjhvw+5E/h6woQXzTaj0Yt2mzjM=
 github.com/sethvargo/go-gcpkms v0.0.0-20200417004547-e50d0c7083d9/go.mod h1:C7JiMBZbJnohGWYmx0vi6mQNGgLxhQX/SWxe0wYxf18=
@@ -343,6 +426,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -417,11 +501,13 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -452,6 +538,7 @@ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -549,10 +636,12 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200413115906-b5235f65be36 h1:j7CmVRD4Kec0+f8VuBAc2Ak2MFfXm5Q2/RxuJLL+76E=
 google.golang.org/genproto v0.0.0-20200413115906-b5235f65be36/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@@ -568,6 +657,7 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
 google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
@@ -576,6 +666,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/android/safetynet.go b/internal/android/safetynet.go
index e1fccc1..798d7fa 100644
--- a/internal/android/safetynet.go
+++ b/internal/android/safetynet.go
@@ -43,8 +43,8 @@ type VerifyOpts struct {
 }
 
 // ValidateAttestation validates the the SafetyNet Attestation from this device
-// matches the properties that we expect based on the applications APIConfig entry.
-// See https://developer.android.com/training/safetynet/attestation#use-response-server
+// matches the properties that we expect based on the AuthorizedApp entry. See
+// https://developer.android.com/training/safetynet/attestation#use-response-server
 // for details on the format of these attestations.
 func ValidateAttestation(ctx context.Context, attestation string, opts *VerifyOpts) error {
 	defer trace.StartRegion(ctx, "ValidateAttestation").End()
@@ -138,8 +138,8 @@ func ValidateAttestation(ctx context.Context, attestation string, opts *VerifyOp
 }
 
 // VerifyOptsFor returns the Android SafetyNet verification options to be used
-// based on the API config, request time, and nonce.
-func VerifyOptsFor(c *model.APIConfig, from time.Time, nonce string) *VerifyOpts {
+// based on the AuthorizedApp configuration, request time, and nonce.
+func VerifyOptsFor(c *model.AuthorizedApp, from time.Time, nonce string) *VerifyOpts {
 	digests := make([]string, len(c.ApkDigestSHA256))
 	copy(digests, c.ApkDigestSHA256)
 	rtn := &VerifyOpts{
diff --git a/internal/apiconfig/config.go b/internal/authorizedapp/config.go
similarity index 70%
rename from internal/apiconfig/config.go
rename to internal/authorizedapp/config.go
index 022e7b0..75fbac6 100644
--- a/internal/apiconfig/config.go
+++ b/internal/authorizedapp/config.go
@@ -12,19 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package apiconfig
+package authorizedapp
 
 import (
 	"time"
 )
 
 type Config struct {
-	// CacheDuration is the amount of time APIConfigs should be cached before
+	// CacheDuration is the amount of time AuthorizedApp should be cached before
 	// being re-read from their provider.
-	CacheDuration time.Duration `envconfig:"APICONFIG_CACHE_DURATION" default:"5m"`
+	CacheDuration time.Duration `envconfig:"AUTHORIZED_APP_CACHE_DURATION" default:"5m"`
 }
 
-// APIConfig implements an interface for setup.
-func (c *Config) APIConfigConfig() *Config {
+// AuthorizedApp implements an interface for setup.
+func (c *Config) AuthorizedApp() *Config {
 	return c
 }
diff --git a/internal/apiconfig/database_provider.go b/internal/authorizedapp/database_provider.go
similarity index 84%
rename from internal/apiconfig/database_provider.go
rename to internal/authorizedapp/database_provider.go
index 3f59476..fcd0233 100644
--- a/internal/apiconfig/database_provider.go
+++ b/internal/authorizedapp/database_provider.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package apiconfig
+package authorizedapp
 
 import (
 	"context"
@@ -41,7 +41,7 @@ type DatabaseProvider struct {
 }
 
 type cacheItem struct {
-	value    *model.APIConfig
+	value    *model.AuthorizedApp
 	cachedAt time.Time
 }
 
@@ -73,7 +73,7 @@ func NewDatabaseProvider(ctx context.Context, db *database.DB, config *Config, o
 }
 
 // AppConfig returns the config for the given app package name.
-func (p *DatabaseProvider) AppConfig(ctx context.Context, name string) (*model.APIConfig, error) {
+func (p *DatabaseProvider) AppConfig(ctx context.Context, name string) (*model.AuthorizedApp, error) {
 	logger := logging.FromContext(ctx)
 
 	// Acquire a read lock first, which allows concurrent readers, to check if
@@ -105,13 +105,13 @@ func (p *DatabaseProvider) AppConfig(ctx context.Context, name string) (*model.A
 	}
 
 	// Load config.
-	config, err := p.loadAPIConfigFromDatabase(ctx, name)
+	config, err := p.loadAuthorizedAppFromDatabase(ctx, name)
 	if err != nil {
-		return nil, fmt.Errorf("apiconfig: %w", err)
+		return nil, fmt.Errorf("authorizedapp: %w", err)
 	}
 
 	// Cache configs.
-	logger.Infof("apiconfig: loaded %v, caching for %s", name, p.cacheDuration)
+	logger.Infof("authorizedapp: loaded %v, caching for %s", name, p.cacheDuration)
 	p.cache[name] = &cacheItem{
 		value:    config,
 		cachedAt: time.Now(),
@@ -127,13 +127,13 @@ func (p *DatabaseProvider) AppConfig(ctx context.Context, name string) (*model.A
 	return config, nil
 }
 
-// loadAPIConfigFromDatabase is a lower-level private API that actually loads and parses
-// a single APIConfigs from the database.
-func (p *DatabaseProvider) loadAPIConfigFromDatabase(ctx context.Context, name string) (*model.APIConfig, error) {
+// loadAuthorizedAppFromDatabase is a lower-level private API that actually loads and parses
+// a single AuthorizedApp from the database.
+func (p *DatabaseProvider) loadAuthorizedAppFromDatabase(ctx context.Context, name string) (*model.AuthorizedApp, error) {
 	logger := logging.FromContext(ctx)
 
-	logger.Infof("apiconfig: loading %v from database", name)
-	config, err := p.database.GetAPIConfig(ctx, p.secretManager, name)
+	logger.Infof("authorizedapp: loading %v from database", name)
+	config, err := p.database.GetAuthorizedApp(ctx, p.secretManager, name)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read %v from database: %w", name, err)
 	}
diff --git a/internal/apiconfig/memory_provider.go b/internal/authorizedapp/memory_provider.go
similarity index 90%
rename from internal/apiconfig/memory_provider.go
rename to internal/authorizedapp/memory_provider.go
index 52526be..31749c2 100644
--- a/internal/apiconfig/memory_provider.go
+++ b/internal/authorizedapp/memory_provider.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package apiconfig
+package authorizedapp
 
 import (
 	"context"
@@ -26,20 +26,20 @@ var _ Provider = (*MemoryProvider)(nil)
 // MemoryProvider is an Provider that stores values in-memory. It is primarily
 // used for testing.
 type MemoryProvider struct {
-	Data map[string]*model.APIConfig
+	Data map[string]*model.AuthorizedApp
 }
 
 // NewMemoryProvider creates a new Provider that reads from a database.
 func NewMemoryProvider(ctx context.Context, _ *Config) (Provider, error) {
 	provider := &MemoryProvider{
-		Data: make(map[string]*model.APIConfig),
+		Data: make(map[string]*model.AuthorizedApp),
 	}
 
 	return provider, nil
 }
 
 // AppConfig returns the config for the given app package name.
-func (p *MemoryProvider) AppConfig(ctx context.Context, name string) (*model.APIConfig, error) {
+func (p *MemoryProvider) AppConfig(ctx context.Context, name string) (*model.AuthorizedApp, error) {
 	val, ok := p.Data[name]
 	if !ok {
 		return nil, AppNotFound
diff --git a/internal/apiconfig/provider.go b/internal/authorizedapp/provider.go
similarity index 87%
rename from internal/apiconfig/provider.go
rename to internal/authorizedapp/provider.go
index 360a3c9..390c37c 100644
--- a/internal/apiconfig/provider.go
+++ b/internal/authorizedapp/provider.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package apiconfig
+package authorizedapp
 
 import (
 	"context"
@@ -25,10 +25,10 @@ import (
 // app with the given name.
 var AppNotFound = errors.New("app not found")
 
-// Provider defines possible APIConfig providers.
+// Provider defines possible AuthorizedApp providers.
 type Provider interface {
 	// AppConfig returns the application-specific configuration for the given
 	// name. An error is returned if the configuration fails to load. An error is
 	// returned if no app with the given name is registered in the system.
-	AppConfig(ctx context.Context, name string) (*model.APIConfig, error)
+	AppConfig(ctx context.Context, name string) (*model.AuthorizedApp, error)
 }
diff --git a/internal/database/apiconfig.go b/internal/database/authorized_app.go
similarity index 92%
rename from internal/database/apiconfig.go
rename to internal/database/authorized_app.go
index eb01e3f..6dbb2c5 100644
--- a/internal/database/apiconfig.go
+++ b/internal/database/authorized_app.go
@@ -26,9 +26,9 @@ import (
 	pgx "github.com/jackc/pgx/v4"
 )
 
-// GetAPIConfig loads a single API config for the given name. If no row exists,
-// this returns nil.
-func (db *DB) GetAPIConfig(ctx context.Context, sm secrets.SecretManager, name string) (*model.APIConfig, error) {
+// GetAuthorizedApp loads a single AuthorizedApp for the given name. If no row
+// exists, this returns nil.
+func (db *DB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, name string) (*model.AuthorizedApp, error) {
 	conn, err := db.pool.Acquire(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("acquiring connection: %v", err)
@@ -41,12 +41,12 @@ func (db *DB) GetAPIConfig(ctx context.Context, sm secrets.SecretManager, name s
 			allowed_past_seconds, allowed_future_seconds, allowed_regions, all_regions,
 			ios_devicecheck_team_id_secret, ios_devicecheck_key_id_secret, ios_devicecheck_private_key_secret
 		FROM
-			APIConfig
+			AuthorizedApp
 		WHERE app_package_name = $1`
 
 	row := conn.QueryRow(ctx, query, name)
 
-	config := model.NewAPIConfig()
+	config := model.NewAuthorizedApp()
 	var allowedRegions []string
 	var allowedPastSeconds, allowedFutureSeconds *int
 	var deviceCheckTeamIDSecret, deviceCheckKeyIDSecret, deviceCheckPrivateKeySecret sql.NullString
diff --git a/internal/model/apiconfig.go b/internal/model/authorized_app.go
similarity index 80%
rename from internal/model/apiconfig.go
rename to internal/model/authorized_app.go
index 9790d04..e91f1af 100644
--- a/internal/model/apiconfig.go
+++ b/internal/model/authorized_app.go
@@ -24,10 +24,10 @@ const (
 	androidDevice = "android"
 )
 
-// APIConfig represents the configuration for a single exposure notification
-// application and their access to and requirements for using the API.
-// DB times of 0 are interpreted to be "unbounded" in that direction.
-type APIConfig struct {
+// AuthorizedApp represents the configuration for a single exposure notification
+// application and their access to and requirements for using the API. DB times
+// of 0 are interpreted to be "unbounded" in that direction.
+type AuthorizedApp struct {
 	AppPackageName  string
 	Platform        string
 	AllowedRegions  map[string]struct{}
@@ -47,25 +47,25 @@ type APIConfig struct {
 	DeviceCheckPrivateKey *ecdsa.PrivateKey
 }
 
-func NewAPIConfig() *APIConfig {
-	return &APIConfig{
+func NewAuthorizedApp() *AuthorizedApp {
+	return &AuthorizedApp{
 		AllowedRegions: make(map[string]struct{}),
 	}
 }
 
 // IsIOS returns true if the platform is equal to `iosDevice`
-func (c *APIConfig) IsIOS() bool {
+func (c *AuthorizedApp) IsIOS() bool {
 	return c.Platform == iosDevice
 }
 
 // IsAndroid returns true if the platform is equal to `android`
-func (c *APIConfig) IsAndroid() bool {
+func (c *AuthorizedApp) IsAndroid() bool {
 	return c.Platform == androidDevice
 }
 
 // IsAllowedRegion returns true if the region is in the list of allowed regions,
 // false otherwise.
-func (c *APIConfig) IsAllowedRegion(s string) bool {
+func (c *AuthorizedApp) IsAllowedRegion(s string) bool {
 	_, ok := c.AllowedRegions[s]
 	return ok
 }
diff --git a/internal/model/exposure.go b/internal/model/exposure.go
index 5786af7..61efe99 100644
--- a/internal/model/exposure.go
+++ b/internal/model/exposure.go
@@ -213,7 +213,7 @@ func (t *Transformer) TransformPublish(inData *Publish, batchTime time.Time) ([]
 	// Regions are a multi-value property, uppercase them for storage.
 	// There is no set of "valid" regions overall, but it is defined
 	// elsewhere by what regions an authorized application may write to.
-	// See `apiconfig.APIConfig`
+	// See `authorizedapp.Config`
 	upcaseRegions := make([]string, len(inData.Regions))
 	for i, r := range inData.Regions {
 		upcaseRegions[i] = strings.ToUpper(r)
diff --git a/internal/publish/config.go b/internal/publish/config.go
index d1579c0..910812c 100644
--- a/internal/publish/config.go
+++ b/internal/publish/config.go
@@ -18,13 +18,13 @@ package publish
 import (
 	"time"
 
-	"github.com/google/exposure-notifications-server/internal/apiconfig"
+	"github.com/google/exposure-notifications-server/internal/authorizedapp"
 	"github.com/google/exposure-notifications-server/internal/database"
 	"github.com/google/exposure-notifications-server/internal/setup"
 )
 
 // Compile-time check to assert this config matches requirements.
-var _ setup.APIConfigProvider = (*Config)(nil)
+var _ setup.AuthorizedAppConfigProvider = (*Config)(nil)
 var _ setup.DBConfigProvider = (*Config)(nil)
 
 // Config represents the configuration and associated environment variables for
@@ -40,13 +40,13 @@ type Config struct {
 	BypassSafetyNet   bool `envconfig:"BYPASS_SAFETYNET"`
 	BypassDeviceCheck bool `envconfig:"BYPASS_DEVICECHECK"`
 
-	APIConfig *apiconfig.Config
-	Database  *database.Config
+	AuthorizedApp *authorizedapp.Config
+	Database      *database.Config
 }
 
-// APIConfigConfig returns the configuration for apiconfig.
-func (c *Config) APIConfigConfig() *apiconfig.Config {
-	return c.APIConfig
+// AuthorizedApp returns the configuration for authorizedapp.
+func (c *Config) AuthorizedAppConfig() *authorizedapp.Config {
+	return c.AuthorizedApp
 }
 
 // DB returns the configuration for the databse.
diff --git a/internal/publish/publish.go b/internal/publish/publish.go
index 1a322e9..d1cd030 100644
--- a/internal/publish/publish.go
+++ b/internal/publish/publish.go
@@ -21,7 +21,7 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/google/exposure-notifications-server/internal/apiconfig"
+	"github.com/google/exposure-notifications-server/internal/authorizedapp"
 	"github.com/google/exposure-notifications-server/internal/database"
 	"github.com/google/exposure-notifications-server/internal/jsonutil"
 	"github.com/google/exposure-notifications-server/internal/logging"
@@ -37,8 +37,8 @@ func NewHandler(ctx context.Context, config *Config, env *serverenv.ServerEnv) (
 	if env.Database() == nil {
 		return nil, fmt.Errorf("missing database in server environment")
 	}
-	if env.APIConfigProvider() == nil {
-		return nil, fmt.Errorf("missing apiconfig provider in server environment")
+	if env.AuthorizedAppProvider() == nil {
+		return nil, fmt.Errorf("missing AuthorizedApp provider in server environment")
 	}
 
 	transformer, err := model.NewTransformer(config.MaxKeysOnPublish, config.MaxIntervalAge, config.TruncateWindow)
@@ -50,20 +50,20 @@ func NewHandler(ctx context.Context, config *Config, env *serverenv.ServerEnv) (
 	logger.Infof("truncate window: %v", config.TruncateWindow)
 
 	return &publishHandler{
-		serverenv:   env,
-		transformer: transformer,
-		config:      config,
-		database:    env.Database(),
-		apiconfig:   env.APIConfigProvider(),
+		serverenv:             env,
+		transformer:           transformer,
+		config:                config,
+		database:              env.Database(),
+		authorizedAppProvider: env.AuthorizedAppProvider(),
 	}, nil
 }
 
 type publishHandler struct {
-	config      *Config
-	serverenv   *serverenv.ServerEnv
-	transformer *model.Transformer
-	database    *database.DB
-	apiconfig   apiconfig.Provider
+	config                *Config
+	serverenv             *serverenv.ServerEnv
+	transformer           *model.Transformer
+	database              *database.DB
+	authorizedAppProvider authorizedapp.Provider
 }
 
 // There is a target normalized latency for this function. This is to help prevent
@@ -83,12 +83,12 @@ func (h *publishHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	appConfig, err := h.apiconfig.AppConfig(ctx, data.AppPackageName)
+	appConfig, err := h.authorizedAppProvider.AppConfig(ctx, data.AppPackageName)
 	if err != nil {
 		// Config loaded, but app with that name isn't registered. This can also
 		// happen if the app was recently registered but the cache hasn't been
 		// refreshed.
-		if err == apiconfig.AppNotFound {
+		if err == authorizedapp.AppNotFound {
 			logger.Errorf("unauthorized app: %v", data.AppPackageName)
 			metrics.WriteInt("publish-app-not-authorized", true, 1)
 			// This returns success to the client.
@@ -99,8 +99,8 @@ func (h *publishHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		// A higher-level configuration error occurred, likely while trying to read
 		// from the database. This is retryable, although won't succeed if the error
 		// isn't transient.
-		logger.Errorf("no API config, dropping data: %v", err)
-		metrics.WriteInt("publish-error-loading-apiconfig", true, 1)
+		logger.Errorf("no AuthorizedApp, dropping data: %v", err)
+		metrics.WriteInt("publish-error-loading-authorizedapp", true, 1)
 		http.Error(w, http.StatusText(http.StatusInternalServerError),
 			http.StatusInternalServerError)
 		return
@@ -137,8 +137,9 @@ func (h *publishHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	} else {
-		logger.Errorf("invalid API configuration for AppPkg: %v, invalid platform", data.AppPackageName)
-		metrics.WriteInt("publish-apiconfig-missing-platform", true, 1)
+		logger.Errorf("invalid AuthorizedApp config %v: invalid platform %v",
+			data.AppPackageName, data.Platform)
+		metrics.WriteInt("publish-authorizedapp-missing-platform", true, 1)
 		// This returns success to the client.
 		w.WriteHeader(http.StatusOK)
 		return
diff --git a/internal/secrets/azure_keyvault.go b/internal/secrets/azure_keyvault.go
new file mode 100644
index 0000000..1307ea0
--- /dev/null
+++ b/internal/secrets/azure_keyvault.go
@@ -0,0 +1,84 @@
+// 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 secrets
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
+	"github.com/Azure/azure-sdk-for-go/services/keyvault/auth"
+)
+
+// Compile-time check to verify implements interface.
+var _ SecretManager = (*AzureKeyVault)(nil)
+
+// AzureKeyVault implements SecretManager.
+type AzureKeyVault struct {
+	client *keyvault.BaseClient
+}
+
+// NewAzureKeyVault creates a new KeyVault that can interact fetch secrets.
+func NewAzureKeyVault(ctx context.Context) (SecretManager, error) {
+	authorizer, err := auth.NewAuthorizerFromEnvironment()
+	if err != nil {
+		return nil, fmt.Errorf("secrets.NewAzureKeyVault: auth: %w", err)
+	}
+
+	client := keyvault.New()
+	client.Authorizer = authorizer
+
+	sm := &AzureKeyVault{
+		client: &client,
+	}
+
+	return sm, nil
+}
+
+// GetSecretValue implements the SecretManager interface. Secrets are specified
+// in the format:
+//
+//     AZURE_KEY_VAULT_NAME/SECRET_NAME/SECRET_VERSION
+//
+// For example:
+//
+//     my-company-vault/api-key/1
+//
+// If the secret version is omitted, the latest version is used.
+func (kv *AzureKeyVault) GetSecretValue(ctx context.Context, name string) (string, error) {
+	// Extract vault, secret, and version.
+	var vaultName, secretName, version string
+	parts := strings.SplitN(name, "/", 3)
+	switch len(parts) {
+	case 0, 1:
+		return "", fmt.Errorf("%v is not a valid secret ref", name)
+	case 2:
+		vaultName, secretName, version = parts[0], parts[1], ""
+	case 3:
+		vaultName, secretName, version = parts[0], parts[1], parts[2]
+	}
+
+	// Lookup in KeyVault
+	vaultURL := fmt.Sprintf("https://%s.vault.azure.net", vaultName)
+	result, err := kv.client.GetSecret(ctx, vaultURL, secretName, version)
+	if err != nil {
+		return "", fmt.Errorf("failed to access secret %v: %w", name, err)
+	}
+	if result.Value == nil {
+		return "", fmt.Errorf("found secret %v, but value was nil", name)
+	}
+	return string(*result.Value), nil
+}
diff --git a/internal/secrets/cacher.go b/internal/secrets/cacher.go
new file mode 100644
index 0000000..eec019d
--- /dev/null
+++ b/internal/secrets/cacher.go
@@ -0,0 +1,91 @@
+// 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 secrets
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/google/exposure-notifications-server/internal/logging"
+)
+
+// Compile-time check to verify implements interface.
+var _ SecretManager = (*Cacher)(nil)
+
+// Cacher is a secret manager implementation that wraps another secret manager
+// and caches secret values.
+type Cacher struct {
+	sm  SecretManager
+	ttl time.Duration
+
+	cache      map[string]*cachedItem
+	cacheMutex sync.Mutex
+}
+
+type cachedItem struct {
+	value    string
+	cachedAt time.Time
+}
+
+// NewCacher creates a new secret manager that caches results for the given ttl.
+func NewCacher(ctx context.Context, f SecretManagerFunc, ttl time.Duration) (SecretManager, error) {
+	sm, err := f(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("cacher: %w", err)
+	}
+
+	return WrapCacher(ctx, sm, ttl)
+}
+
+// WrapCacher wraps an existing SecretManager with caching.
+func WrapCacher(ctx context.Context, sm SecretManager, ttl time.Duration) (SecretManager, error) {
+	return &Cacher{
+		sm:    sm,
+		ttl:   ttl,
+		cache: make(map[string]*cachedItem),
+	}, nil
+}
+
+// GetSecretValue implements the SecretManager interface, but caches values and
+// retrieves them from the cache.
+func (sm *Cacher) GetSecretValue(ctx context.Context, name string) (string, error) {
+	logger := logging.FromContext(ctx)
+
+	// Lock
+	sm.cacheMutex.Lock()
+	defer sm.cacheMutex.Unlock()
+
+	// Lookup in cache
+	if i, ok := sm.cache[name]; ok && time.Since(i.cachedAt) < sm.ttl {
+		logger.Debugf("loaded secret %v from cache", name)
+		return i.value, nil
+	}
+
+	// Delegate lookup to parent sm.
+	plaintext, err := sm.sm.GetSecretValue(ctx, name)
+	if err != nil {
+		return "", err
+	}
+
+	// Cache value
+	sm.cache[name] = &cachedItem{
+		value:    plaintext,
+		cachedAt: time.Now(),
+	}
+
+	return plaintext, nil
+}
diff --git a/internal/secrets/gcp_secret_manager.go b/internal/secrets/gcp_secret_manager.go
index 1e6632e..2e969a6 100644
--- a/internal/secrets/gcp_secret_manager.go
+++ b/internal/secrets/gcp_secret_manager.go
@@ -12,102 +12,43 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package secrets defines a minimum abstract interface for a secret manager.
-// Allows for a different implementation to be bound within the servernv.ServeEnv
 package secrets
 
 import (
 	"context"
 	"fmt"
-	"sync"
-	"time"
-
-	"github.com/google/exposure-notifications-server/internal/logging"
 
 	secretmanager "cloud.google.com/go/secretmanager/apiv1"
 	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
 )
 
-const (
-	// defaultCacheDuration is how long to cache a secret.
-	defaultCacheDuration = 5 * time.Minute
-)
+// Compile-time check to verify implements interface.
+var _ SecretManager = (*GCPSecretManager)(nil)
 
+// GCPSecretManager implements SecretManager.
 type GCPSecretManager struct {
 	client *secretmanager.Client
-
-	cache         map[string]*item
-	cacheDuration time.Duration
-	cacheMutex    sync.Mutex
-}
-
-type item struct {
-	value     string
-	expiresAt int64
-}
-
-// Option is an option to the setup.
-type Option func(*GCPSecretManager) *GCPSecretManager
-
-// WithCacheDuration creates an Option to install a specific secret manager to use.
-func WithCacheDuration(d time.Duration) Option {
-	return func(s *GCPSecretManager) *GCPSecretManager {
-		s.cacheDuration = d
-		return s
-	}
 }
 
-func NewGCPSecretManager(ctx context.Context, opts ...Option) (SecretManager, error) {
+// NewGCPSecretManager creates a new secret manager for GCP.
+func NewGCPSecretManager(ctx context.Context) (SecretManager, error) {
 	client, err := secretmanager.NewClient(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("secretmanager.NewClient: %w", err)
 	}
 
 	sm := &GCPSecretManager{
-		client:        client,
-		cache:         make(map[string]*item),
-		cacheDuration: defaultCacheDuration,
-	}
-
-	for _, f := range opts {
-		sm = f(sm)
+		client: client,
 	}
 
 	return sm, nil
 }
 
-// GetSecretValue implements the SecretManager interface.
+// GetSecretValue implements the SecretManager interface. Secret names should be
+// of the format:
+//
+//     projects/my-project/secrets/my-secret/versions/123
 func (sm *GCPSecretManager) GetSecretValue(ctx context.Context, name string) (string, error) {
-	logger := logging.FromContext(ctx)
-
-	// Lock
-	sm.cacheMutex.Lock()
-	defer sm.cacheMutex.Unlock()
-
-	// Lookup in cache
-	if i, ok := sm.cache[name]; ok && i.expiresAt <= time.Now().UnixNano() {
-		logger.Debugf("loaded secret %v from cache", name)
-		return i.value, nil
-	}
-
-	// Lookup in secret manager
-	plaintext, err := sm.accessSecret(ctx, name)
-	if err != nil {
-		return "", err
-	}
-
-	// Cache value
-	sm.cache[name] = &item{
-		value:     plaintext,
-		expiresAt: time.Now().Add(sm.cacheDuration).UnixNano(),
-	}
-
-	return plaintext, nil
-}
-
-// accessSecret accesses the secret from secret manager.
-func (sm *GCPSecretManager) accessSecret(ctx context.Context, name string) (string, error) {
-	// Call the API.
 	result, err := sm.client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
 		Name: name,
 	})
diff --git a/internal/secrets/hashicorp_vault.go b/internal/secrets/hashicorp_vault.go
new file mode 100644
index 0000000..573a398
--- /dev/null
+++ b/internal/secrets/hashicorp_vault.go
@@ -0,0 +1,86 @@
+// 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 secrets
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	vaultapi "github.com/hashicorp/vault/api"
+)
+
+// Compile-time check to verify implements interface.
+var _ SecretManager = (*HashiCorpVault)(nil)
+
+type HashiCorpVault struct {
+	client *vaultapi.Client
+}
+
+// NewHashiCorpVault fetches secrets from HashiCorp Vault.
+func NewHashiCorpVault(ctx context.Context) (SecretManager, error) {
+	client, err := vaultapi.NewClient(nil)
+	if err != nil {
+		return nil, fmt.Errorf("secrets.NewHashiCorpVault: client: %w", err)
+	}
+
+	sm := &HashiCorpVault{
+		client: client,
+	}
+
+	return sm, nil
+}
+
+// GetSecretValue implements the SecretManager interface. Secrets are specified
+// as the path to the secret in Vault. Secrets are expected to have the string
+// value for the secret in a key named "value".
+//
+// For example:
+//
+//     /secret/data/my-secret?version=5 #=> { "value": "dajkfl32ip2" }
+//
+// Note: this technically allows you to fetch dynamic secrets, but this library
+// makes no attempt at renewing leases!
+func (kv *HashiCorpVault) GetSecretValue(ctx context.Context, name string) (string, error) {
+	secret, err := kv.client.Logical().Read(name)
+	if err != nil {
+		return "", fmt.Errorf("failed to access secret %v: %w", name, err)
+	}
+	if secret == nil || secret.Data == nil {
+		return "", fmt.Errorf("found secret %v, but value was nil", name)
+	}
+
+	// Check if the "value" key is present.
+	raw, ok := secret.Data["value"]
+	if !ok {
+		return "", fmt.Errorf("found secret %v, does not have 'value' key", name)
+	}
+
+	// Vault values are map[string]interface{}, so coerce to a string.
+	switch typ := raw.(type) {
+	case string:
+		return typ, nil
+	case []byte:
+		return string(typ), nil
+	case bool:
+		return strconv.FormatBool(typ), nil
+	case int, int8, int16, int32, int64:
+		return fmt.Sprintf("%d", typ), nil
+	case uint, uint8, uint16, uint32, uint64:
+		return fmt.Sprintf("%d", typ), nil
+	default:
+		return "", fmt.Errorf("found secret %v, but is of unknown type %T", name, typ)
+	}
+}
diff --git a/internal/secrets/secrets.go b/internal/secrets/secrets.go
index c279592..8ea4d51 100644
--- a/internal/secrets/secrets.go
+++ b/internal/secrets/secrets.go
@@ -23,3 +23,6 @@ import "context"
 type SecretManager interface {
 	GetSecretValue(ctx context.Context, name string) (string, error)
 }
+
+// SecretManagerFunc is a func that returns a secret manager or error.
+type SecretManagerFunc func(ctx context.Context) (SecretManager, error)
diff --git a/internal/serverenv/env.go b/internal/serverenv/env.go
index 79f9911..70c4d39 100644
--- a/internal/serverenv/env.go
+++ b/internal/serverenv/env.go
@@ -20,7 +20,7 @@ import (
 	"crypto"
 	"fmt"
 
-	"github.com/google/exposure-notifications-server/internal/apiconfig"
+	"github.com/google/exposure-notifications-server/internal/authorizedapp"
 	"github.com/google/exposure-notifications-server/internal/database"
 	"github.com/google/exposure-notifications-server/internal/metrics"
 	"github.com/google/exposure-notifications-server/internal/secrets"
@@ -33,12 +33,12 @@ type ExporterFunc func(context.Context) metrics.Exporter
 
 // ServerEnv represents latent environment configuration for servers in this application.
 type ServerEnv struct {
-	secretManager     secrets.SecretManager
-	keyManager        signing.KeyManager
-	blobstore         storage.Blobstore
-	exporter          metrics.ExporterFromContext
-	apiConfigProvider apiconfig.Provider
-	database          *database.DB
+	authorizedAppProvider authorizedapp.Provider
+	blobstore             storage.Blobstore
+	database              *database.DB
+	exporter              metrics.ExporterFromContext
+	keyManager            signing.KeyManager
+	secretManager         secrets.SecretManager
 }
 
 // Option defines function types to modify the ServerEnv on creation.
@@ -68,10 +68,10 @@ func WithDatabase(db *database.DB) Option {
 	}
 }
 
-// WithAPIConfigProvider installs a provider of APIConfig.
-func WithAPIConfigProvider(p apiconfig.Provider) Option {
+// WithAuthorizedAppProvider installs a provider for an authorized app.
+func WithAuthorizedAppProvider(p authorizedapp.Provider) Option {
 	return func(s *ServerEnv) *ServerEnv {
-		s.apiConfigProvider = p
+		s.authorizedAppProvider = p
 		return s
 	}
 }
@@ -120,8 +120,8 @@ func (s *ServerEnv) Blobstore() storage.Blobstore {
 	return s.blobstore
 }
 
-func (s *ServerEnv) APIConfigProvider() apiconfig.Provider {
-	return s.apiConfigProvider
+func (s *ServerEnv) AuthorizedAppProvider() authorizedapp.Provider {
+	return s.authorizedAppProvider
 }
 
 func (s *ServerEnv) Database() *database.DB {
diff --git a/internal/setup/setup.go b/internal/setup/setup.go
index cf62b32..8f9e0b2 100644
--- a/internal/setup/setup.go
+++ b/internal/setup/setup.go
@@ -17,8 +17,9 @@ package setup
 import (
 	"context"
 	"fmt"
+	"time"
 
-	"github.com/google/exposure-notifications-server/internal/apiconfig"
+	"github.com/google/exposure-notifications-server/internal/authorizedapp"
 	"github.com/google/exposure-notifications-server/internal/database"
 	"github.com/google/exposure-notifications-server/internal/envconfig"
 	"github.com/google/exposure-notifications-server/internal/metrics"
@@ -34,10 +35,10 @@ type DBConfigProvider interface {
 	DB() *database.Config
 }
 
-// APIConfigProvider signals that the config provided knows how to configure
-// and requires a APIConfigs.
-type APIConfigProvider interface {
-	APIConfigConfig() *apiconfig.Config
+// AuthorizedAppConfigProvider signals that the config provided knows how to
+// configure authorized apps.
+type AuthorizedAppConfigProvider interface {
+	AuthorizedAppConfig() *authorizedapp.Config
 }
 
 // KeyManagerProvider is a marker interface indicating the KeyManagerProvider should be installed.
@@ -57,7 +58,8 @@ type Defer func()
 func Setup(ctx context.Context, config DBConfigProvider) (*serverenv.ServerEnv, Defer, error) {
 	// Can be changed with a different secret manager interface.
 	// TODO(mikehelmick): Make this extensible to other providers.
-	sm, err := secrets.NewGCPSecretManager(ctx)
+	// 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)
 	}
@@ -96,15 +98,15 @@ func Setup(ctx context.Context, config DBConfigProvider) (*serverenv.ServerEnv,
 	}
 	opts = append(opts, serverenv.WithDatabase(db))
 
-	// APIConfig must come after database setup due to the dependency.
-	if typ, ok := config.(APIConfigProvider); ok {
-		provider, err := apiconfig.NewDatabaseProvider(ctx, db, typ.APIConfigConfig(), apiconfig.WithSecretManager(sm))
+	// AuthorizedApp must come after database setup due to the dependency.
+	if typ, ok := config.(AuthorizedAppConfigProvider); ok {
+		provider, err := authorizedapp.NewDatabaseProvider(ctx, db, typ.AuthorizedAppConfig(), authorizedapp.WithSecretManager(sm))
 		if err != nil {
 			// Ensure the database is closed on an error.
 			defer db.Close(ctx)
-			return nil, nil, fmt.Errorf("unable to create APIConfig provider: %v", err)
+			return nil, nil, fmt.Errorf("unable to create AuthorizedApp provider: %v", err)
 		}
-		opts = append(opts, serverenv.WithAPIConfigProvider(provider))
+		opts = append(opts, serverenv.WithAuthorizedAppProvider(provider))
 	}
 
 	return serverenv.New(ctx, opts...), func() { db.Close(ctx) }, nil
diff --git a/internal/storage/filesystem_storage.go b/internal/storage/filesystem_storage.go
new file mode 100644
index 0000000..7d1f7d5
--- /dev/null
+++ b/internal/storage/filesystem_storage.go
@@ -0,0 +1,57 @@
+// 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 is an interface over Google Cloud Storage.
+package storage
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+)
+
+// Compile-time check to verify implements interface.
+var _ Blobstore = (*FilesystemStorage)(nil)
+
+// FilesystemStorage implements Blobstore and provdes the ability
+// write files to the filesystem.
+type FilesystemStorage struct{}
+
+// NewFilesystemStorage creates a Blobsstore compatible storage for the
+// filesystem.
+func NewFilesystemStorage(ctx context.Context) (Blobstore, error) {
+	return &FilesystemStorage{}, nil
+}
+
+// CreateObject creates a new object on the filesystem or overwrites an existing
+// one.
+func (s *FilesystemStorage) CreateObject(ctx context.Context, folder, filename string, contents []byte) error {
+	pth := filepath.Join(folder, filename)
+	if err := ioutil.WriteFile(pth, contents, 0644); err != nil {
+		return fmt.Errorf("failed to create object: %w", err)
+	}
+	return nil
+}
+
+// DeleteObject deletes an object from the filesystem. It returns nil if the
+// object was deleted or if the object no longer exists.
+func (s *FilesystemStorage) DeleteObject(ctx context.Context, folder, filename string) error {
+	pth := filepath.Join(folder, filename)
+	if err := os.Remove(pth); err != nil && !os.IsNotExist(err) {
+		return fmt.Errorf("failed to delete object: %w", err)
+	}
+	return nil
+}
diff --git a/internal/storage/google_cloud_storage.go b/internal/storage/google_cloud_storage.go
index e6148d5..d8920b6 100644
--- a/internal/storage/google_cloud_storage.go
+++ b/internal/storage/google_cloud_storage.go
@@ -23,6 +23,9 @@ import (
 	"cloud.google.com/go/storage"
 )
 
+// Compile-time check to verify implements interface.
+var _ Blobstore = (*GoogleCloudStorage)(nil)
+
 // GoogleCloudStorage implements the Blob interface and provdes the ability
 // write files to Google Cloud Storage.
 type GoogleCloudStorage struct {
diff --git a/internal/verification/verify.go b/internal/verification/verify.go
index b950600..9206070 100644
--- a/internal/verification/verify.go
+++ b/internal/verification/verify.go
@@ -31,7 +31,7 @@ var (
 
 // VerifyRegions checks the request regions against the regions allowed by
 // the configuration for the application.
-func VerifyRegions(cfg *model.APIConfig, data *model.Publish) error {
+func VerifyRegions(cfg *model.AuthorizedApp, data *model.Publish) error {
 	if cfg == nil {
 		return fmt.Errorf("no allowed regions configured")
 	}
@@ -52,7 +52,7 @@ func VerifyRegions(cfg *model.APIConfig, data *model.Publish) error {
 
 // VerifySafetyNet verifies the Android SafetyNet device attestation against the
 // allowed configuration for the application.
-func VerifySafetyNet(ctx context.Context, requestTime time.Time, cfg *model.APIConfig, publish *model.Publish) error {
+func VerifySafetyNet(ctx context.Context, requestTime time.Time, cfg *model.AuthorizedApp, publish *model.Publish) error {
 	if cfg == nil {
 		return fmt.Errorf("cannot enforce SafetyNet, missing config")
 	}
@@ -66,7 +66,7 @@ func VerifySafetyNet(ctx context.Context, requestTime time.Time, cfg *model.APIC
 }
 
 // VerifyDeviceCheck verifies an iOS DeviceCheck token against the Apple API.
-func VerifyDeviceCheck(ctx context.Context, cfg *model.APIConfig, data *model.Publish) error {
+func VerifyDeviceCheck(ctx context.Context, cfg *model.AuthorizedApp, data *model.Publish) error {
 	if cfg == nil {
 		return fmt.Errorf("cannot enforce DeviceCheck, missing config")
 	}
diff --git a/migrations/000020_rename_apiconfig_to_authorized_app.down.sql b/migrations/000020_rename_apiconfig_to_authorized_app.down.sql
new file mode 100644
index 0000000..d7ef654
--- /dev/null
+++ b/migrations/000020_rename_apiconfig_to_authorized_app.down.sql
@@ -0,0 +1,22 @@
+-- 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.
+
+BEGIN;
+
+ALTER TABLE AuthorizedApp RENAME TO APIConfig;
+ALTER INDEX authorized_app_pkey RENAME TO apiconfig_pkey;
+ALTER INDEX authorized_app_app_package_name_idx RENAME TO apiconfig_app_package_name_idx;
+
+
+END;
diff --git a/migrations/000020_rename_apiconfig_to_authorized_app.up.sql b/migrations/000020_rename_apiconfig_to_authorized_app.up.sql
new file mode 100644
index 0000000..7147287
--- /dev/null
+++ b/migrations/000020_rename_apiconfig_to_authorized_app.up.sql
@@ -0,0 +1,21 @@
+-- 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.
+
+BEGIN;
+
+ALTER TABLE APIConfig RENAME TO AuthorizedApp;
+ALTER INDEX apiconfig_pkey RENAME TO authorized_app_pkey;
+ALTER INDEX apiconfig_app_package_name_idx RENAME TO authorized_app_app_package_name_idx;
+
+END;


Test patch:
diff --git a/internal/android/safetynet_test.go b/internal/android/safetynet_test.go
index 8763b3d..3ffdb70 100644
--- a/internal/android/safetynet_test.go
+++ b/internal/android/safetynet_test.go
@@ -192,11 +192,11 @@ func TestVerifyOptsFor(t *testing.T) {
 	testTime := time.Date(2020, 1, 13, 5, 6, 4, 6, time.Local)
 
 	cases := []struct {
-		cfg  *model.APIConfig
+		cfg  *model.AuthorizedApp
 		opts *VerifyOpts
 	}{
 		{
-			cfg: &model.APIConfig{
+			cfg: &model.AuthorizedApp{
 				AppPackageName:    "foo",
 				CTSProfileMatch:   true,
 				BasicIntegrity:    true,
@@ -213,7 +213,7 @@ func TestVerifyOptsFor(t *testing.T) {
 			},
 		},
 		{
-			cfg: &model.APIConfig{
+			cfg: &model.AuthorizedApp{
 				AppPackageName:    "foo",
 				CTSProfileMatch:   false,
 				BasicIntegrity:    true,
@@ -230,7 +230,7 @@ func TestVerifyOptsFor(t *testing.T) {
 			},
 		},
 		{
-			cfg: &model.APIConfig{
+			cfg: &model.AuthorizedApp{
 				AppPackageName:    "foo",
 				ApkDigestSHA256:   []string{"bar"},
 				CTSProfileMatch:   false,
diff --git a/internal/database/apiconfig_test.go b/internal/database/authorized_app_test.go
similarity index 91%
rename from internal/database/apiconfig_test.go
rename to internal/database/authorized_app_test.go
index 2078c6a..d5b7f0a 100644
--- a/internal/database/apiconfig_test.go
+++ b/internal/database/authorized_app_test.go
@@ -42,7 +42,7 @@ func (s *testSecretManager) GetSecretValue(ctx context.Context, name string) (st
 	return v, nil
 }
 
-func TestGetAPIConfig(t *testing.T) {
+func TestGetAuthorizedApp(t *testing.T) {
 	if testDB == nil {
 		t.Skip("no test DB")
 	}
@@ -75,17 +75,17 @@ func TestGetAPIConfig(t *testing.T) {
 		name string
 		sql  string
 		args []interface{}
-		exp  *model.APIConfig
+		exp  *model.AuthorizedApp
 		err  bool
 	}{
 		{
 			name: "bare",
 			sql: `
-				INSERT INTO APIConfig (app_package_name, platform, allowed_regions)
+				INSERT INTO AuthorizedApp (app_package_name, platform, allowed_regions)
 				VALUES ($1, $2, $3)
 			`,
 			args: []interface{}{"myapp", "ios", []string{"US"}},
-			exp: &model.APIConfig{
+			exp: &model.AuthorizedApp{
 				AppPackageName:  "myapp",
 				Platform:        "ios",
 				AllowedRegions:  map[string]struct{}{"US": {}},
@@ -96,13 +96,13 @@ func TestGetAPIConfig(t *testing.T) {
 		{
 			name: "allowed_past_time",
 			sql: `
-				INSERT INTO APIConfig (
+				INSERT INTO AuthorizedApp (
 					app_package_name, platform, allowed_regions,
 					allowed_past_seconds
 				) VALUES ($1, $2, $3, $4)
 			`,
 			args: []interface{}{"myapp", "ios", []string{"US"}, 1800},
-			exp: &model.APIConfig{
+			exp: &model.AuthorizedApp{
 				AppPackageName:  "myapp",
 				Platform:        "ios",
 				AllowedRegions:  map[string]struct{}{"US": {}},
@@ -114,13 +114,13 @@ func TestGetAPIConfig(t *testing.T) {
 		{
 			name: "allowed_future_time",
 			sql: `
-				INSERT INTO APIConfig (
+				INSERT INTO AuthorizedApp (
 					app_package_name, platform, allowed_regions,
 					allowed_future_seconds
 				) VALUES ($1, $2, $3, $4)
 			`,
 			args: []interface{}{"myapp", "ios", []string{"US"}, 1800},
-			exp: &model.APIConfig{
+			exp: &model.AuthorizedApp{
 				AppPackageName:    "myapp",
 				Platform:          "ios",
 				AllowedRegions:    map[string]struct{}{"US": {}},
@@ -132,13 +132,13 @@ func TestGetAPIConfig(t *testing.T) {
 		{
 			name: "ios_devicecheck",
 			sql: `
-				INSERT INTO APIConfig (
+				INSERT INTO AuthorizedApp (
 					app_package_name, platform, allowed_regions,
 					ios_devicecheck_team_id_secret, ios_devicecheck_key_id_secret, ios_devicecheck_private_key_secret
 				) VALUES ($1, $2, $3, $4, $5, $6)
 			`,
 			args: []interface{}{"myapp", "ios", []string{"US"}, "team_id", "key_id", "private_key"},
-			exp: &model.APIConfig{
+			exp: &model.AuthorizedApp{
 				AppPackageName:        "myapp",
 				Platform:              "ios",
 				AllowedRegions:        map[string]struct{}{"US": {}},
@@ -173,7 +173,7 @@ func TestGetAPIConfig(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			config, err := testDB.GetAPIConfig(ctx, sm, "myapp")
+			config, err := testDB.GetAuthorizedApp(ctx, sm, "myapp")
 			if (err != nil) != c.err {
 				t.Fatal(err)
 			}
diff --git a/internal/database/database_test.go b/internal/database/database_test.go
index aa851dd..5ff3b5f 100644
--- a/internal/database/database_test.go
+++ b/internal/database/database_test.go
@@ -128,9 +128,9 @@ func resetTestDB(t *testing.T) {
 	defer conn.Release()
 
 	_, err = conn.Exec(ctx, `
-		TRUNCATE 
+		TRUNCATE
 			FederationInQuery, FederationInSync, FederationOutAuthorization,
-			Exposure, APIConfig,
+			Exposure, AuthorizedApp,
 			ExportConfig, ExportBatch, ExportFile
 	`)
 	if err != nil {
diff --git a/internal/model/apiconfig_test.go b/internal/model/authorized_app_test.go
similarity index 91%
rename from internal/model/apiconfig_test.go
rename to internal/model/authorized_app_test.go
index 5b27f41..23aa5e0 100644
--- a/internal/model/apiconfig_test.go
+++ b/internal/model/authorized_app_test.go
@@ -18,8 +18,8 @@ import (
 	"testing"
 )
 
-func TestBaseAPIConfig(t *testing.T) {
-	cfg := NewAPIConfig()
+func TestBaseAuthorizedApp(t *testing.T) {
+	cfg := NewAuthorizedApp()
 	if cfg.IsIOS() {
 		t.Errorf("cfg.IoIOS, got true, want false")
 	}
diff --git a/internal/secrets/cacher_test.go b/internal/secrets/cacher_test.go
new file mode 100644
index 0000000..2b25062
--- /dev/null
+++ b/internal/secrets/cacher_test.go
@@ -0,0 +1,82 @@
+// 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 secrets
+
+import (
+	"context"
+	"testing"
+	"time"
+)
+
+type testSecretManager struct {
+	value string
+	hits  int
+}
+
+func (sm *testSecretManager) GetSecretValue(ctx context.Context, name string) (string, error) {
+	sm.hits++
+	return sm.value, nil
+}
+
+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)
+	}
+
+	// Read the value once, which should cache it.
+	if _, err := cached.GetSecretValue(ctx, "secret"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Change the value.
+	sm.value = "second"
+
+	// Read the value a few more times.
+	for i := 0; i < 5; i++ {
+		got, err := cached.GetSecretValue(ctx, "secret")
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if want := "first"; got != want {
+			t.Errorf("expected %q to be %q", got, want)
+		}
+
+		if sm.hits > 1 {
+			t.Errorf("cache miss: %d", sm.hits)
+		}
+	}
+
+	// Wait for cache to expire.
+	time.Sleep(300 * time.Millisecond)
+
+	// Try again - should miss
+	got, err := cached.GetSecretValue(ctx, "secret")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if want := "second"; got != want {
+		t.Errorf("expected %q to be %q", got, want)
+	}
+
+	if sm.hits != 2 {
+		t.Errorf("expected another hit: %d", sm.hits)
+	}
+}
diff --git a/internal/storage/filesystem_storage_test.go b/internal/storage/filesystem_storage_test.go
new file mode 100644
index 0000000..c900f4e
--- /dev/null
+++ b/internal/storage/filesystem_storage_test.go
@@ -0,0 +1,124 @@
+// 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 (
+	"bytes"
+	"context"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestFilesystemStorage_CreateObject(t *testing.T) {
+	tmp, err := ioutil.TempDir("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() { os.RemoveAll(tmp) })
+
+	cases := []struct {
+		name     string
+		folder   string
+		filepath string
+		contents []byte
+		err      bool
+	}{
+		{
+			name:     "default",
+			folder:   tmp,
+			filepath: "myfile",
+			contents: []byte("contents"),
+		},
+		{
+			name:     "bad_path",
+			folder:   "/path/that/definitely/doesnt/exist",
+			filepath: "myfile",
+			contents: []byte("contents"),
+			err:      true,
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			storage, err := NewFilesystemStorage(ctx)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			err = storage.CreateObject(ctx, tc.folder, tc.filepath, tc.contents)
+			if (err != nil) != tc.err {
+				t.Fatal(err)
+			}
+
+			if !tc.err {
+				contents, err := ioutil.ReadFile(filepath.Join(tc.folder, tc.filepath))
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				if !bytes.Equal(contents, tc.contents) {
+					t.Errorf("expected %q to be %q ", contents, tc.contents)
+				}
+			}
+		})
+	}
+}
+
+func TestFilesystemStorage_DeleteObject(t *testing.T) {
+	f, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cases := []struct {
+		name     string
+		folder   string
+		filepath string
+	}{
+		{
+			name:     "default",
+			folder:   filepath.Dir(f.Name()),
+			filepath: filepath.Base(f.Name()),
+		},
+		{
+			name:     "not_exist",
+			folder:   filepath.Dir(f.Name()),
+			filepath: "not-exist",
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			storage, err := NewFilesystemStorage(ctx)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if err = storage.DeleteObject(ctx, tc.folder, tc.filepath); err != nil {
+				t.Fatal(err)
+			}
+		})
+	}
+}
diff --git a/internal/verification/verify_test.go b/internal/verification/verify_test.go
index f1a1b9c..adefc28 100644
--- a/internal/verification/verify_test.go
+++ b/internal/verification/verify_test.go
@@ -30,11 +30,11 @@ const (
 )
 
 func TestVerifyRegions(t *testing.T) {
-	allRegions := &model.APIConfig{
+	allRegions := &model.AuthorizedApp{
 		AppPackageName:  appPkgName,
 		AllowAllRegions: true,
 	}
-	usCaRegions := &model.APIConfig{
+	usCaRegions := &model.AuthorizedApp{
 		AppPackageName: appPkgName,
 		AllowedRegions: make(map[string]struct{}),
 	}
@@ -44,7 +44,7 @@ func TestVerifyRegions(t *testing.T) {
 	cases := []struct {
 		Data *model.Publish
 		Msg  string
-		Cfg  *model.APIConfig
+		Cfg  *model.AuthorizedApp
 	}{
 		{
 			&model.Publish{Regions: []string{"US"}},
@@ -89,7 +89,7 @@ func TestVerifyRegions(t *testing.T) {
 }
 
 func TestVerifySafetyNet(t *testing.T) {
-	allRegions := &model.APIConfig{
+	allRegions := &model.AuthorizedApp{
 		AppPackageName:  appPkgName,
 		AllowAllRegions: true,
 	}
@@ -97,7 +97,7 @@ func TestVerifySafetyNet(t *testing.T) {
 	cases := []struct {
 		Data              *model.Publish
 		Msg               string
-		Cfg               *model.APIConfig
+		Cfg               *model.AuthorizedApp
 		AttestationResult error
 	}{
 		{