aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-12-24 08:30:27 -0800
committerMike Crute <mike@crute.us>2022-12-24 08:30:27 -0800
commit4aac2b6026d27d4eba660b674bdb1f34bcfbfc54 (patch)
tree62eb040afb6d7f56f9bfb0af69cfc17ec2060a8b
parented1504c2826f6a5d406dd72e51f5a90b77ffea45 (diff)
downloadcloud-identity-broker-4aac2b6026d27d4eba660b674bdb1f34bcfbfc54.tar.bz2
cloud-identity-broker-4aac2b6026d27d4eba660b674bdb1f34bcfbfc54.tar.xz
cloud-identity-broker-4aac2b6026d27d4eba660b674bdb1f34bcfbfc54.zip
Use Vault for IAM users
-rw-r--r--app/controllers/api_account.go8
-rw-r--r--app/controllers/aws.go51
-rw-r--r--app/models/account.go12
-rw-r--r--app/models/session_key.go2
-rw-r--r--cloud/aws/aws.go137
-rw-r--r--cmd/web/server.go12
-rw-r--r--go.mod5
-rw-r--r--go.sum9
8 files changed, 128 insertions, 108 deletions
diff --git a/app/controllers/api_account.go b/app/controllers/api_account.go
index 815daf4..cecd334 100644
--- a/app/controllers/api_account.go
+++ b/app/controllers/api_account.go
@@ -2,14 +2,12 @@ package controllers
2 2
3import ( 3import (
4 "context" 4 "context"
5 "fmt"
6 "net/http" 5 "net/http"
7 "time" 6 "time"
8 7
9 "code.crute.us/mcrute/cloud-identity-broker/app" 8 "code.crute.us/mcrute/cloud-identity-broker/app"
10 "code.crute.us/mcrute/cloud-identity-broker/app/middleware" 9 "code.crute.us/mcrute/cloud-identity-broker/app/middleware"
11 "code.crute.us/mcrute/cloud-identity-broker/app/models" 10 "code.crute.us/mcrute/cloud-identity-broker/app/models"
12 "code.crute.us/mcrute/cloud-identity-broker/cloud/aws"
13 11
14 glecho "code.crute.us/mcrute/golib/echo" 12 glecho "code.crute.us/mcrute/golib/echo"
15 "code.crute.us/mcrute/golib/echo/controller" 13 "code.crute.us/mcrute/golib/echo/controller"
@@ -90,7 +88,7 @@ func (h *APIAccountHandler) HandleGet(c echo.Context) error {
90 // details about the account so they should only be visible to users who 88 // details about the account so they should only be visible to users who
91 // can administer the account. 89 // can administer the account.
92 if !a.CanBeModifiedBy(p) { 90 if !a.CanBeModifiedBy(p) {
93 a.VaultMaterial = "" 91 a.AdminVaultMaterial = ""
94 a.Users = nil 92 a.Users = nil
95 } 93 }
96 94
@@ -136,7 +134,7 @@ func (h *APIAccountHandler) HandlePut(c echo.Context) error {
136 a.AccountNumber = in.AccountNumber 134 a.AccountNumber = in.AccountNumber
137 a.Name = in.Name 135 a.Name = in.Name
138 a.ConsoleSessionDuration = in.ConsoleSessionDuration 136 a.ConsoleSessionDuration = in.ConsoleSessionDuration
139 a.VaultMaterial = in.VaultMaterial 137 a.AdminVaultMaterial = in.AdminVaultMaterial
140 a.DefaultRegion = in.DefaultRegion 138 a.DefaultRegion = in.DefaultRegion
141 a.Users = in.Users 139 a.Users = in.Users
142 140
@@ -181,12 +179,14 @@ func (h *APIAccountHandler) HandlePost(c echo.Context) error {
181 } 179 }
182 } 180 }
183 181
182 /* TODO: Validate that the vault material exists
184 if err := aws.ValidateVaultMaterial(in.VaultMaterial); err != nil { 183 if err := aws.ValidateVaultMaterial(in.VaultMaterial); err != nil {
185 return &echo.HTTPError{ 184 return &echo.HTTPError{
186 Code: http.StatusBadRequest, 185 Code: http.StatusBadRequest,
187 Message: fmt.Sprintf("Unable to access Vault material: %s", err), 186 Message: fmt.Sprintf("Unable to access Vault material: %s", err),
188 } 187 }
189 } 188 }
189 */
190 190
191 if err := h.Store.Put(context.Background(), &in); err != nil { 191 if err := h.Store.Put(context.Background(), &in); err != nil {
192 return echo.ErrInternalServerError 192 return echo.ErrInternalServerError
diff --git a/app/controllers/aws.go b/app/controllers/aws.go
index 5b1765d..4f32942 100644
--- a/app/controllers/aws.go
+++ b/app/controllers/aws.go
@@ -2,11 +2,14 @@ package controllers
2 2
3import ( 3import (
4 "context" 4 "context"
5 "sync"
5 6
6 "code.crute.us/mcrute/cloud-identity-broker/app/middleware" 7 "code.crute.us/mcrute/cloud-identity-broker/app/middleware"
7 "code.crute.us/mcrute/cloud-identity-broker/app/models" 8 "code.crute.us/mcrute/cloud-identity-broker/app/models"
8 "code.crute.us/mcrute/cloud-identity-broker/cloud/aws" 9 "code.crute.us/mcrute/cloud-identity-broker/cloud/aws"
9 10
11 "code.crute.us/mcrute/golib/secrets"
12
10 "github.com/labstack/echo/v4" 13 "github.com/labstack/echo/v4"
11) 14)
12 15
@@ -20,7 +23,49 @@ type requestContext struct {
20// This capability does common permission checks and populates a request 23// This capability does common permission checks and populates a request
21// context with user, account, and AWS API information. 24// context with user, account, and AWS API information.
22type AWSAPI struct { 25type AWSAPI struct {
23 Store models.AccountStore 26 Store models.AccountStore
27 Secrets secrets.Client
28 cache sync.Map // of aws.AWSClient
29}
30
31func (h *AWSAPI) getClientFor(ctx context.Context, a *models.Account) (aws.AWSClient, error) {
32 var client aws.AWSClient
33
34 if cv, ok := h.cache.Load(a.ShortName); !ok {
35 client, err := aws.NewAWSClientFromAccount(ctx, a, h.Secrets)
36 if err != nil {
37 return nil, err
38 }
39
40 cv, _ = h.cache.LoadOrStore(a.ShortName, client)
41 client = cv.(aws.AWSClient)
42 } else {
43 client = cv.(aws.AWSClient)
44 }
45
46 return client, nil
47}
48
49// Preload enumerates all managed accounts and pre-loads all of the
50// clients into the cache.
51//
52// This exists because there is replication delay of around 5-10 seconds
53// for IAM users into other regions so these should be created as early
54// in the process lifecycle as possible.
55func (h *AWSAPI) Preload(ctx context.Context) []error {
56 accounts, err := h.Store.List(ctx)
57 if err != nil {
58 return []error{err}
59 }
60
61 errors := []error{}
62 for _, a := range accounts {
63 _, err = h.getClientFor(ctx, a)
64 if err != nil {
65 errors = append(errors, err)
66 }
67 }
68 return errors
24} 69}
25 70
26// GetContext checks that the user is authenticated and is authorized to access 71// GetContext checks that the user is authenticated and is authorized to access
@@ -38,7 +83,7 @@ func (h *AWSAPI) GetContext(c echo.Context) (*requestContext, error) {
38 return nil, echo.NotFoundHandler(c) 83 return nil, echo.NotFoundHandler(c)
39 } 84 }
40 85
41 ac, err := aws.NewAWSClientFromAccount(account) 86 client, err := h.getClientFor(c.Request().Context(), account)
42 if err != nil { 87 if err != nil {
43 c.Logger().Errorf("Error building AWS client: %w", err) 88 c.Logger().Errorf("Error building AWS client: %w", err)
44 return nil, echo.ErrInternalServerError 89 return nil, echo.ErrInternalServerError
@@ -47,6 +92,6 @@ func (h *AWSAPI) GetContext(c echo.Context) (*requestContext, error) {
47 return &requestContext{ 92 return &requestContext{
48 Account: account, 93 Account: account,
49 Principal: principal, 94 Principal: principal,
50 AWS: ac, 95 AWS: client,
51 }, nil 96 }, nil
52} 97}
diff --git a/app/models/account.go b/app/models/account.go
index af29c3e..346354c 100644
--- a/app/models/account.go
+++ b/app/models/account.go
@@ -25,8 +25,9 @@ type Account struct {
25 AccountType string `json:"account_type"` 25 AccountType string `json:"account_type"`
26 AccountNumber int `json:"account_number"` 26 AccountNumber int `json:"account_number"`
27 Name string `json:"name"` 27 Name string `json:"name"`
28 ConsoleSessionDuration time.Duration `json:"console_session_duration, omitempty"` 28 ConsoleSessionDuration time.Duration `json:"console_session_duration,omitempty"`
29 VaultMaterial string `json:"vault_material,omitempty"` 29 AdminVaultMaterial string `json:"admin_vault_material,omitempty"`
30 AssumedRoleARN string `json:"assumed_role_arn"`
30 DefaultRegion string `json:"default_region"` 31 DefaultRegion string `json:"default_region"`
31 Users []string `json:"users,omitempty"` 32 Users []string `json:"users,omitempty"`
32 Deleted *time.Time `json:"deleted,omitempty" bson:"deleted,omitempty"` 33 Deleted *time.Time `json:"deleted,omitempty" bson:"deleted,omitempty"`
@@ -43,10 +44,9 @@ func (a *Account) CanBeModifiedBy(u *User) bool {
43type MongoDbAccountStore struct { 44type MongoDbAccountStore struct {
44 Db *mongodb.Mongo 45 Db *mongodb.Mongo
45 46
46 // ReturnDeleted will allow all methods to return deleted items. By default 47 // ReturnDeleted will allow all methods to return deleted items. items
47 // items where the Deleted field is set will not be returned. This should 48 // where the Deleted field is set will not be returned. Non-admin
48 // be the common cast for most code using this store but in some Admin 49 // use-cases should leave this set to false.
49 // use-cases it would be useful to show deleted accounts.
50 ReturnDeleted bool 50 ReturnDeleted bool
51} 51}
52 52
diff --git a/app/models/session_key.go b/app/models/session_key.go
index c8b327e..b75d6c4 100644
--- a/app/models/session_key.go
+++ b/app/models/session_key.go
@@ -52,7 +52,7 @@ type SessionKey struct {
52 NotBefore *time.Time 52 NotBefore *time.Time
53 PublicKey crypto.PublicKey 53 PublicKey crypto.PublicKey
54 PrivateKey *ecdsa.PrivateKey 54 PrivateKey *ecdsa.PrivateKey
55 ExposePrivateKeysInJSON bool `"-" json:"-" bson:"-"` 55 ExposePrivateKeysInJSON bool `json:"-" bson:"-"`
56} 56}
57 57
58func GenerateSessionKey(ttl time.Duration) (*SessionKey, error) { 58func GenerateSessionKey(ttl time.Duration) (*SessionKey, error) {
diff --git a/cloud/aws/aws.go b/cloud/aws/aws.go
index 36ac338..3229dd2 100644
--- a/cloud/aws/aws.go
+++ b/cloud/aws/aws.go
@@ -1,6 +1,7 @@
1package aws 1package aws
2 2
3import ( 3import (
4 "context"
4 "encoding/json" 5 "encoding/json"
5 "fmt" 6 "fmt"
6 "io/ioutil" 7 "io/ioutil"
@@ -9,9 +10,10 @@ import (
9 "strconv" 10 "strconv"
10 11
11 "code.crute.us/mcrute/cloud-identity-broker/app/models" 12 "code.crute.us/mcrute/cloud-identity-broker/app/models"
13 "code.crute.us/mcrute/golib/secrets"
12 14
13 "code.crute.us/mcrute/golib/vault"
14 "github.com/aws/aws-sdk-go/aws" 15 "github.com/aws/aws-sdk-go/aws"
16 "github.com/aws/aws-sdk-go/aws/awserr"
15 "github.com/aws/aws-sdk-go/aws/credentials" 17 "github.com/aws/aws-sdk-go/aws/credentials"
16 "github.com/aws/aws-sdk-go/aws/session" 18 "github.com/aws/aws-sdk-go/aws/session"
17 "github.com/aws/aws-sdk-go/service/ec2" 19 "github.com/aws/aws-sdk-go/service/ec2"
@@ -27,28 +29,18 @@ type Region struct {
27 Enabled bool 29 Enabled bool
28} 30}
29 31
32// AWSClient is a client for working with the AWS APIs.
33//
34// Instances of AWSClient are safe for concurrent access.
30type AWSClient interface { 35type AWSClient interface {
31 AssumeRole(string, *string) (*sts.Credentials, error) 36 AssumeRole(string, *string) (*sts.Credentials, error)
32 GetFederationURL(string, string) (string, error) 37 GetFederationURL(string, string) (string, error)
33 GetRegionList() ([]*Region, error) 38 GetRegionList() ([]*Region, error)
34} 39}
35 40
36// account models the account configuration stored in Vault for an AWS account
37// with assumable roles that are stored within a kv JSON record.
38type account struct {
39 AccessKeyId string
40 SecretAccessKey string
41 Roles map[string]struct {
42 ARN string
43 ExternalId string
44 }
45}
46
47type client struct { 41type client struct {
48 AccessKeyId string 42 Credentials credentials.Value
49 SecretAccessKey string
50 ARN string 43 ARN string
51 ExternalId string
52 ConsoleSessionDurationSecs int64 44 ConsoleSessionDurationSecs int64
53} 45}
54 46
@@ -62,61 +54,22 @@ var _ AWSClient = (*client)(nil)
62// which is used as the scope for this AWS client. Thus even if an account has 54// which is used as the scope for this AWS client. Thus even if an account has
63// multiple roles there must be one instance of the AWS client per account/role 55// multiple roles there must be one instance of the AWS client per account/role
64// pair. 56// pair.
65func NewAWSClientFromAccount(a *models.Account) (AWSClient, error) { 57func NewAWSClientFromAccount(ctx context.Context, a *models.Account, sc secrets.Client) (AWSClient, error) {
66 var ac account 58 u, _, err := sc.AWSIAMUser(ctx, a.AdminVaultMaterial)
67 if err := vault.GetVaultKeyStruct(a.VaultMaterial, &ac); err != nil { 59 if err != nil {
68 return nil, err 60 return nil, err
69 } 61 }
70 62
71 r, ok := ac.Roles[a.ShortName]
72 if !ok {
73 return nil, fmt.Errorf("No roles for account %s in vault response", a.ShortName)
74 }
75
76 return &client{ 63 return &client{
77 AccessKeyId: ac.AccessKeyId, 64 Credentials: credentials.Value{
78 SecretAccessKey: ac.SecretAccessKey, 65 AccessKeyID: u.AccessKeyId,
79 ARN: r.ARN, 66 SecretAccessKey: u.SecretAccessKey,
80 ExternalId: r.ExternalId, 67 },
68 ARN: a.AssumedRoleARN,
81 ConsoleSessionDurationSecs: a.ConsoleSessionDurationSecs(), 69 ConsoleSessionDurationSecs: a.ConsoleSessionDurationSecs(),
82 }, nil 70 }, nil
83} 71}
84 72
85// ValidateVaultMaterial is used to check that a Vault material can be accessed
86// and that the shape of that material is correct for an AWS access key and
87// role list.
88//
89// This should be used for admission control for the creation of new accounts.
90func ValidateVaultMaterial(m string) error {
91 var ac account
92 if err := vault.GetVaultKeyStruct(m, &ac); err != nil {
93 return fmt.Errorf("Unable to access vault material: %w", err)
94 }
95
96 if ac.AccessKeyId == "" {
97 return fmt.Errorf("AccessKeyId is empty")
98 }
99
100 if ac.SecretAccessKey == "" {
101 return fmt.Errorf("SecretAccessKey is empty")
102 }
103
104 if len(ac.Roles) == 0 {
105 return fmt.Errorf("No roles specified")
106 }
107
108 for k, r := range ac.Roles {
109 if r.ARN == "" {
110 return fmt.Errorf("ARN for role %s is empty", k)
111 }
112 if r.ExternalId == "" {
113 return fmt.Errorf("ExternalId for role %s is empty", k)
114 }
115 }
116
117 return nil
118}
119
120// AssumeRole uses an IAM user credential with higher privilege to assume a 73// AssumeRole uses an IAM user credential with higher privilege to assume a
121// role in an AWS account and region. It returns the STS credentials. 74// role in an AWS account and region. It returns the STS credentials.
122// 75//
@@ -124,32 +77,34 @@ func ValidateVaultMaterial(m string) error {
124// regions AWS has been siloing assumed role credentials to that region so it's 77// regions AWS has been siloing assumed role credentials to that region so it's
125// important to use the correct regional endpoint to fetch the credentials. 78// important to use the correct regional endpoint to fetch the credentials.
126// 79//
127// Note that this is not simply a passthrough to Vault's AWS backend because 80// Note that this is not simply a passthrough to Vault's AWS backend
128// the Vault backend works by assuming roles and when assuming roles with an 81// because the Vault backend will only call AssumeRole within the region
129// assumed role AWS limits the chained role lifetime to 1 hour which doesn't 82// of the root account configured in Vault and clients of this method
130// work depending on how the upstream web application tied to this client 83// need to be able to make the correct calls in the region of their
131// works. This method instead uses a long-lived IAM user credential to assume a 84// choice.
132// role, which has a limited lifetime which is typically greater than 1 hour.
133func (a *client) AssumeRole(user string, region *string) (*sts.Credentials, error) { 85func (a *client) AssumeRole(user string, region *string) (*sts.Credentials, error) {
134 if region != nil && *region == "global" { 86 if region != nil && *region == "global" {
135 region = nil 87 region = nil
136 } 88 }
137 89
138 c := sts.New(session.New(&aws.Config{ 90 s, err := session.NewSessionWithOptions(session.Options{
139 Region: region, 91 Config: aws.Config{
140 Credentials: credentials.NewStaticCredentials( 92 Region: region,
141 a.AccessKeyId, 93 Credentials: credentials.NewStaticCredentialsFromCreds(a.Credentials),
142 a.SecretAccessKey, 94 },
143 "", 95 SharedConfigState: session.SharedConfigDisable,
144 ), 96 })
145 })) 97 if err != nil {
146 result, err := c.AssumeRole(&sts.AssumeRoleInput{ 98 return nil, err
147 ExternalId: aws.String(a.ExternalId), 99 }
100
101 result, err := sts.New(s).AssumeRole(&sts.AssumeRoleInput{
148 RoleArn: aws.String(a.ARN), 102 RoleArn: aws.String(a.ARN),
149 RoleSessionName: aws.String(user), 103 RoleSessionName: aws.String(user),
150 DurationSeconds: aws.Int64(a.ConsoleSessionDurationSecs), 104 DurationSeconds: aws.Int64(a.ConsoleSessionDurationSecs),
151 }) 105 })
152 if err != nil { 106 if err != nil {
107 fmt.Printf("AWS AssumeRole Error: %s\n", err.(awserr.Error).Message())
153 return nil, err 108 return nil, err
154 } 109 }
155 110
@@ -167,16 +122,24 @@ func (a *client) GetRegionList() ([]*Region, error) {
167 return nil, err 122 return nil, err
168 } 123 }
169 124
170 ec2c := ec2.New(session.New(&aws.Config{ 125 s, err := session.NewSessionWithOptions(session.Options{
171 Region: defaultRegion, 126 Config: aws.Config{
172 Credentials: credentials.NewStaticCredentials( 127 Region: defaultRegion,
173 *r.AccessKeyId, 128 Credentials: credentials.NewStaticCredentials(
174 *r.SecretAccessKey, 129 *r.AccessKeyId,
175 *r.SessionToken, 130 *r.SecretAccessKey,
176 ), 131 *r.SessionToken,
177 })) 132 ),
133 },
134 SharedConfigState: session.SharedConfigDisable,
135 })
136 if err != nil {
137 return nil, err
138 }
178 139
179 ro, err := ec2c.DescribeRegions(&ec2.DescribeRegionsInput{AllRegions: aws.Bool(true)}) 140 ro, err := ec2.New(s).DescribeRegions(&ec2.DescribeRegionsInput{
141 AllRegions: aws.Bool(true),
142 })
180 if err != nil { 143 if err != nil {
181 return nil, err 144 return nil, err
182 } 145 }
diff --git a/cmd/web/server.go b/cmd/web/server.go
index 9c29544..82e0f75 100644
--- a/cmd/web/server.go
+++ b/cmd/web/server.go
@@ -130,7 +130,17 @@ func setupApplication(ctx context.Context, cfg app.Config, s *glecho.EchoWrapper
130 as := &models.MongoDbAccountStore{Db: mongo} 130 as := &models.MongoDbAccountStore{Db: mongo}
131 us := &models.MongoDbUserStore{Db: mongo} 131 us := &models.MongoDbUserStore{Db: mongo}
132 132
133 aws := &controllers.AWSAPI{Store: as} 133 aws := &controllers.AWSAPI{
134 Store: as,
135 Secrets: vc,
136 }
137
138 if errs := aws.Preload(ctx); len(errs) > 0 {
139 for _, err := range errs {
140 log.Printf("Error preloading AWS accounts: %s", err)
141 }
142 log.Fatalf("Could not preload all AWS accounts")
143 }
134 144
135 ghCred := &app.GitHubOauthCreds{} 145 ghCred := &app.GitHubOauthCreds{}
136 if _, err := vc.Secret(ctx, cfg.GitHubOauthCreds, &ghCred); err != nil { 146 if _, err := vc.Secret(ctx, cfg.GitHubOauthCreds, &ghCred); err != nil {
diff --git a/go.mod b/go.mod
index b1202d4..b86cee7 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module code.crute.us/mcrute/cloud-identity-broker
3go 1.18 3go 1.18
4 4
5replace ( 5replace (
6 code.crute.us/mcrute/golib => ../golib 6 code.crute.us/mcrute/golib/secrets => ../golib/secrets
7 golang.org/x/crypto => ../third_party/golang/x/crypto 7 golang.org/x/crypto => ../third_party/golang/x/crypto
8) 8)
9 9
@@ -14,7 +14,6 @@ require (
14 code.crute.us/mcrute/golib/db/mongodb/v2 v2.0.0 14 code.crute.us/mcrute/golib/db/mongodb/v2 v2.0.0
15 code.crute.us/mcrute/golib/echo v0.9.3 15 code.crute.us/mcrute/golib/echo v0.9.3
16 code.crute.us/mcrute/golib/secrets v0.2.0 16 code.crute.us/mcrute/golib/secrets v0.2.0
17 code.crute.us/mcrute/golib/vault v0.2.4
18 github.com/aws/aws-sdk-go v1.42.4 17 github.com/aws/aws-sdk-go v1.42.4
19 github.com/labstack/echo/v4 v4.6.1 18 github.com/labstack/echo/v4 v4.6.1
20 github.com/prometheus/client_golang v1.11.0 19 github.com/prometheus/client_golang v1.11.0
@@ -29,6 +28,7 @@ require (
29 code.crute.us/mcrute/golib v0.5.1 // indirect 28 code.crute.us/mcrute/golib v0.5.1 // indirect
30 code.crute.us/mcrute/golib/clients/dns v0.1.0 // indirect 29 code.crute.us/mcrute/golib/clients/dns v0.1.0 // indirect
31 code.crute.us/mcrute/golib/clients/netbox v0.1.0 // indirect 30 code.crute.us/mcrute/golib/clients/netbox v0.1.0 // indirect
31 code.crute.us/mcrute/golib/vault v0.2.4 // indirect
32 github.com/armon/go-metrics v0.3.10 // indirect 32 github.com/armon/go-metrics v0.3.10 // indirect
33 github.com/armon/go-radix v1.0.0 // indirect 33 github.com/armon/go-radix v1.0.0 // indirect
34 github.com/beorn7/perks v1.0.1 // indirect 34 github.com/beorn7/perks v1.0.1 // indirect
@@ -96,4 +96,5 @@ require (
96 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect 96 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
97 google.golang.org/grpc v1.42.0 // indirect 97 google.golang.org/grpc v1.42.0 // indirect
98 google.golang.org/protobuf v1.27.1 // indirect 98 google.golang.org/protobuf v1.27.1 // indirect
99 gopkg.in/yaml.v2 v2.4.0 // indirect
99) 100)
diff --git a/go.sum b/go.sum
index 4864035..038cea4 100644
--- a/go.sum
+++ b/go.sum
@@ -45,6 +45,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
45cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 45cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
46cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 46cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
47cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 47cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
48code.crute.us/mcrute/golib v0.5.1 h1:z66+VQxqas98nPYeGEdJvkHrQwxd2mISVm6qzaBJwF8=
49code.crute.us/mcrute/golib v0.5.1/go.mod h1:dukLPhs1H8dxtkhXtpJZYo/bMzefLRbdRj9Tj67wdaQ=
48code.crute.us/mcrute/golib/cli v0.2.2 h1:1MgyEYCyZ2oJBs/FrztMmxJoh0v+7j21VsWXBTIWsqw= 50code.crute.us/mcrute/golib/cli v0.2.2 h1:1MgyEYCyZ2oJBs/FrztMmxJoh0v+7j21VsWXBTIWsqw=
49code.crute.us/mcrute/golib/cli v0.2.2/go.mod h1:vc2TpQ5J/3zRfcWq6sclmU0EmJI8xygpOij77VJ8EK8= 51code.crute.us/mcrute/golib/cli v0.2.2/go.mod h1:vc2TpQ5J/3zRfcWq6sclmU0EmJI8xygpOij77VJ8EK8=
50code.crute.us/mcrute/golib/clients/autocert/v2 v2.0.0 h1:MTS65Npib7DFnsNZ5Fs7EYXkK2ITEqdZQ18kBd3FdPk= 52code.crute.us/mcrute/golib/clients/autocert/v2 v2.0.0 h1:MTS65Npib7DFnsNZ5Fs7EYXkK2ITEqdZQ18kBd3FdPk=
@@ -59,8 +61,6 @@ code.crute.us/mcrute/golib/db/mongodb/v2 v2.0.0 h1:v4AYsbesoDeAMMbwS43WzqywNm0w0
59code.crute.us/mcrute/golib/db/mongodb/v2 v2.0.0/go.mod h1:3dFJwm2MtCb312eHdHnK/w8D1lwgCeewa/2hztw89kE= 61code.crute.us/mcrute/golib/db/mongodb/v2 v2.0.0/go.mod h1:3dFJwm2MtCb312eHdHnK/w8D1lwgCeewa/2hztw89kE=
60code.crute.us/mcrute/golib/echo v0.9.3 h1:rg14mTarLpRrpHPJN5X1k1VTFplo3rgOPCXPRf3xzmY= 62code.crute.us/mcrute/golib/echo v0.9.3 h1:rg14mTarLpRrpHPJN5X1k1VTFplo3rgOPCXPRf3xzmY=
61code.crute.us/mcrute/golib/echo v0.9.3/go.mod h1:mcmhqsSWD/+ECdrd0Sh9u9XGtukXdLPVHc88sKg/gJo= 63code.crute.us/mcrute/golib/echo v0.9.3/go.mod h1:mcmhqsSWD/+ECdrd0Sh9u9XGtukXdLPVHc88sKg/gJo=
62code.crute.us/mcrute/golib/secrets v0.2.0 h1:ENPZ+GEkdcmbFak0kHrI9JdSmVmBHxdzoKnjgvPHf5E=
63code.crute.us/mcrute/golib/secrets v0.2.0/go.mod h1:O1ypm8JirXI4SekwNCHwQbfsieDQJxeRNwZYoot6fvw=
64code.crute.us/mcrute/golib/vault v0.2.4 h1:lNc1hq26e/UAGBqxQlZiFffOXZSNEcEkKUzU3oRJ8Eg= 64code.crute.us/mcrute/golib/vault v0.2.4 h1:lNc1hq26e/UAGBqxQlZiFffOXZSNEcEkKUzU3oRJ8Eg=
65code.crute.us/mcrute/golib/vault v0.2.4/go.mod h1:23C5g8O0zaeFfo7v6sCO0RKgnHIiHM9ku+ASOWHJD9k= 65code.crute.us/mcrute/golib/vault v0.2.4/go.mod h1:23C5g8O0zaeFfo7v6sCO0RKgnHIiHM9ku+ASOWHJD9k=
66dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 66dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -515,8 +515,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
515github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 515github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
516github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 516github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
517github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 517github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
518github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
519github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 518github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
519github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
520github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 520github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
521github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 521github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
522github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 522github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
@@ -989,6 +989,7 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
989gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 989gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
990gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 990gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
991gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 991gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
992gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
992gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 993gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
993gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 994gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
994gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 995gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -1004,8 +1005,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1004gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 1005gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
1005gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 1006gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
1006gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 1007gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1007gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
1008gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 1008gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1009gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1009honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 1010honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
1010honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 1011honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
1011honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 1012honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=