├── .gitignore ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── go.mod ├── plugin ├── backend_test.go ├── auth_attempt.go ├── config.go ├── role.go ├── backend.go ├── attestor.go ├── path_login.go ├── path_config.go ├── path_role.go └── attestor_test.go ├── main.go ├── LICENSE ├── Taskfile.yml ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | release 2 | vault-plugin-auth-openstack 3 | cover.out 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: ["*"] 4 | tags-ignore: ["v*"] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | name: Build 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Install tools 15 | run: | 16 | curl -sL https://taskfile.dev/install.sh | sh 17 | sudo mv ./bin/task /usr/local/bin 18 | 19 | - name: Build 20 | run: task build 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/summerwind/vault-plugin-auth-openstack 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gophercloud/gophercloud v0.8.0 7 | github.com/gophercloud/utils v0.0.0-20200204043447-9864b6f1f12f 8 | github.com/hashicorp/go-hclog v0.9.2 9 | github.com/hashicorp/vault/api v1.0.5-0.20200117231345-460d63e36490 10 | github.com/hashicorp/vault/sdk v0.1.14-0.20200121232954-73f411823aa0 11 | github.com/kr/pretty v0.1.0 // indirect 12 | github.com/mitchellh/mapstructure v1.1.2 13 | ) 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | name: Release 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | 13 | - name: Install tools 14 | run: | 15 | curl -sL https://taskfile.dev/install.sh | sh 16 | sudo mv bin/task /usr/local/bin 17 | curl -L -O https://github.com/tcnksm/ghr/releases/download/v0.13.0/ghr_v0.13.0_linux_amd64.tar.gz 18 | tar zxvf ghr_v0.13.0_linux_amd64.tar.gz 19 | sudo mv ghr_v0.13.0_linux_amd64/ghr /usr/local/bin 20 | 21 | - name: Upload artifacts 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: task github-release 25 | -------------------------------------------------------------------------------- /plugin/backend_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/vault/sdk/helper/logging" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | func newTestBackend(t *testing.T) (logical.Backend, logical.Storage) { 14 | config := &logical.BackendConfig{ 15 | Logger: logging.NewVaultLogger(hclog.Trace), 16 | System: &logical.StaticSystemView{ 17 | DefaultLeaseTTLVal: time.Hour * 12, 18 | MaxLeaseTTLVal: time.Hour * 24, 19 | }, 20 | StorageView: &logical.InmemStorage{}, 21 | } 22 | 23 | b, err := Factory(context.Background(), config) 24 | if err != nil { 25 | t.Fatalf("unable to create backend: %v", err) 26 | } 27 | 28 | return b, config.StorageView 29 | } 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/go-hclog" 7 | "github.com/hashicorp/vault/api" 8 | "github.com/hashicorp/vault/sdk/plugin" 9 | 10 | openstack "github.com/summerwind/vault-plugin-auth-openstack/plugin" 11 | ) 12 | 13 | func main() { 14 | meta := &api.PluginAPIClientMeta{} 15 | flags := meta.FlagSet() 16 | flags.Parse(os.Args[1:]) 17 | 18 | tlsConfig := meta.GetTLSConfig() 19 | tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) 20 | 21 | err := plugin.Serve(&plugin.ServeOpts{ 22 | BackendFactoryFunc: openstack.Factory, 23 | TLSProviderFunc: tlsProviderFunc, 24 | }) 25 | if err != nil { 26 | logger := hclog.New(&hclog.LoggerOptions{}) 27 | logger.Error("plugin shutting down", "error", err) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Moto Ishizawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | vars: 4 | NAME: vault-plugin-auth-openstack 5 | VERSION: 0.4.3 6 | 7 | tasks: 8 | build: 9 | deps: [test] 10 | cmds: 11 | - CGO_ENABLED=0 go build . 12 | test: 13 | cmds: 14 | - go vet ./... 15 | - go test -v -coverprofile=cover.out ./... 16 | cover: 17 | deps: [test] 18 | cmds: 19 | - go tool cover -html=cover.out 20 | package: 21 | cmds: 22 | - GOOS={{.OS}} GOARCH={{.ARCH}} CGO_ENABLED=0 go build . 23 | - shasum -a 256 {{.NAME}} > sha256sum.txt 24 | - tar -czf release/{{.NAME}}_{{.OS}}_{{.ARCH}}.tar.gz {{.NAME}} sha256sum.txt 25 | - echo `cat sha256sum.txt` "({{.OS}}_{{.ARCH}})" 26 | - rm -rf {{.NAME}} sha256sum.txt 27 | release: 28 | deps: [test] 29 | cmds: 30 | - mkdir -p release 31 | - task: package 32 | vars: {OS: "linux", ARCH: "amd64"} 33 | - task: package 34 | vars: {OS: "linux", ARCH: "arm64"} 35 | - task: package 36 | vars: {OS: "linux", ARCH: "arm"} 37 | - task: package 38 | vars: {OS: "darwin", ARCH: "amd64"} 39 | github-release: 40 | deps: [release] 41 | cmds: 42 | - ghr v{{.VERSION}} release/ 43 | clean: 44 | cmds: 45 | - rm -rf {{.NAME}} release cover.out 46 | -------------------------------------------------------------------------------- /plugin/auth_attempt.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | type AuthAttempt struct { 13 | Name string `json:"name" structs:"name" mapstructure:"name"` 14 | Deadline time.Time `json:"deadline" structs:"deadline" mapstructure:"deadline"` 15 | Count int `json:"count" structs:"count" mapstructure:"count"` 16 | } 17 | 18 | func readAuthAttempt(ctx context.Context, s logical.Storage, name string) (*AuthAttempt, error) { 19 | entry, err := s.Get(ctx, fmt.Sprintf("auth_attempt/%s", name)) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | if entry == nil { 25 | return nil, nil 26 | } 27 | 28 | attempt := &AuthAttempt{} 29 | err = entry.DecodeJSON(attempt) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return attempt, nil 35 | } 36 | 37 | func updateAuthAttempt(ctx context.Context, s logical.Storage, attempt *AuthAttempt) error { 38 | if attempt.Name == "" { 39 | return errors.New("invalid attempt name") 40 | } 41 | 42 | entry, err := logical.StorageEntryJSON(fmt.Sprintf("auth_attempt/%s", attempt.Name), attempt) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = s.Put(ctx, entry) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func cleanupAuthAttempt(ctx context.Context, s logical.Storage) (int, error) { 56 | count := 0 57 | 58 | keys, err := s.List(ctx, "auth_attempt/") 59 | if err != nil { 60 | return 0, err 61 | } 62 | 63 | for _, key := range keys { 64 | attempt, err := readAuthAttempt(ctx, s, key) 65 | if err != nil { 66 | return 0, err 67 | } 68 | 69 | if time.Now().After(attempt.Deadline) { 70 | err := s.Delete(ctx, fmt.Sprintf("auth_attempt/%s", key)) 71 | if err != nil { 72 | return 0, err 73 | } 74 | count += 1 75 | } 76 | } 77 | 78 | return count, nil 79 | } 80 | -------------------------------------------------------------------------------- /plugin/config.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/vault/sdk/logical" 7 | ) 8 | 9 | type Config struct { 10 | AuthURL string `json:"auth_url" structs:"auth_url" mapstructure:"auth_url"` 11 | Token string `json:"token" structs:"token" mapstructure:"token"` 12 | UserID string `json:"user_id" structs:"user_id" mapstructure:"user_id"` 13 | Username string `json:"username" structs:"username" mapstructure:"username"` 14 | Password string `json:"password" structs:"password" mapstructure:"password"` 15 | ProjectID string `json:"project_id" structs:"project_id" mapstructure:"project_id"` 16 | ProjectName string `json:"project_name" structs:"project_name" mapstructure:"project_name"` 17 | TenantID string `json:"tenant_id" structs:"tenant_id" mapstructure:"tenant_id"` 18 | TenantName string `json:"tenant_name" structs:"tenant_name" mapstructure:"tenant_name"` 19 | UserDomainID string `json:"user_domain_id" structs:"user_domain_id" mapstructure:"user_domain_id"` 20 | UserDomainName string `json:"user_domain_name" structs:"user_domain_name" mapstructure:"user_domain_name"` 21 | ProjectDomainID string `json:"project_domain_id" structs:"project_domain_id" mapstructure:"project_domain_id"` 22 | ProjectDomainName string `json:"project_domain_name" structs:"project_domain_name" mapstructure:"project_domain_name"` 23 | DomainID string `json:"domain_id" structs:"domain_id" mapstructure:"domain_id"` 24 | DomainName string `json:"domain_name" structs:"domain_name" mapstructure:"domain_name"` 25 | } 26 | 27 | func readConfig(ctx context.Context, s logical.Storage) (*Config, error) { 28 | entry, err := s.Get(ctx, "config") 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if entry == nil { 34 | return nil, nil 35 | } 36 | 37 | config := &Config{} 38 | err = entry.DecodeJSON(config) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return config, nil 44 | } 45 | -------------------------------------------------------------------------------- /plugin/role.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | type Role struct { 13 | Name string `json:"name" structs:"name" mapstructure:"name"` 14 | Policies []string `json:"policies" structs:"policies" mapstructure:"policies"` 15 | TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"` 16 | MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` 17 | Period time.Duration `json:"period" structs:"period" mapstructure:"period"` 18 | MetadataKey string `json:"metadata_key" structs:"metadata_key" mapstructure:"metadata_key"` 19 | TenantID string `json:"tenant_id" structs:"tenant_id" mapstructure:"tenant_id"` 20 | UserID string `json:"user_id" structs:"user_id" mapstructure:"user_id"` 21 | AuthPeriod time.Duration `json:"auth_period" structs:"auth_period" mapstructure:"auth_period"` 22 | AuthLimit int `json:"auth_limit" structs:"auth_limit" mapstructure:"auth_limit"` 23 | } 24 | 25 | func (r *Role) Validate(sys logical.SystemView) (warnings []string, err error) { 26 | warnings = []string{} 27 | 28 | if r.MetadataKey == "" { 29 | return warnings, errors.New("metadata_key cannot be empty") 30 | } 31 | 32 | if r.AuthPeriod < time.Duration(0) { 33 | return warnings, errors.New("auth_period cannot be negative") 34 | } 35 | 36 | if r.AuthLimit < 0 { 37 | return warnings, errors.New("auth_limit cannot be negative") 38 | } 39 | 40 | defaultLeaseTTL := sys.DefaultLeaseTTL() 41 | if r.TTL > defaultLeaseTTL { 42 | warnings = append(warnings, fmt.Sprintf( 43 | "Given ttl of %d seconds greater than current mount/system default of %d seconds; ttl will be capped at login time", 44 | r.TTL/time.Second, defaultLeaseTTL/time.Second)) 45 | } 46 | 47 | defaultMaxTTL := sys.MaxLeaseTTL() 48 | if r.MaxTTL > defaultMaxTTL { 49 | warnings = append(warnings, fmt.Sprintf( 50 | "Given max_ttl of %d seconds greater than current mount/system default of %d seconds; max_ttl will be capped at login time", 51 | r.MaxTTL/time.Second, defaultMaxTTL/time.Second)) 52 | } 53 | 54 | if r.MaxTTL < time.Duration(0) { 55 | return warnings, errors.New("max_ttl cannot be negative") 56 | } 57 | 58 | if r.MaxTTL != 0 && r.MaxTTL < r.TTL { 59 | return warnings, errors.New("ttl should be shorter than max_ttl") 60 | } 61 | 62 | if r.Period > sys.MaxLeaseTTL() { 63 | return warnings, fmt.Errorf("'period' of '%s' is greater than the backend's maximum lease TTL of '%s'", r.Period, sys.MaxLeaseTTL()) 64 | } 65 | 66 | return warnings, nil 67 | } 68 | 69 | func readRole(ctx context.Context, s logical.Storage, name string) (*Role, error) { 70 | entry, err := s.Get(ctx, fmt.Sprintf("role/%s", name)) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if entry == nil { 76 | return nil, nil 77 | } 78 | 79 | role := &Role{} 80 | err = entry.DecodeJSON(role) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return role, nil 86 | } 87 | -------------------------------------------------------------------------------- /plugin/backend.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/gophercloud/gophercloud" 9 | "github.com/gophercloud/gophercloud/openstack" 10 | "github.com/gophercloud/utils/openstack/clientconfig" 11 | "github.com/hashicorp/vault/sdk/framework" 12 | "github.com/hashicorp/vault/sdk/logical" 13 | ) 14 | 15 | const ( 16 | help = "The OpenStack backend plugin allows authentication for OpenStack instances." 17 | ) 18 | 19 | type OpenStackAuthBackend struct { 20 | *framework.Backend 21 | client *gophercloud.ServiceClient 22 | clientMutex sync.RWMutex 23 | } 24 | 25 | func NewBackend() *OpenStackAuthBackend { 26 | b := &OpenStackAuthBackend{} 27 | 28 | b.Backend = &framework.Backend{ 29 | BackendType: logical.TypeCredential, 30 | Invalidate: b.invalidateHandler, 31 | PeriodicFunc: b.periodicHandler, 32 | AuthRenew: b.authRenewHandler, 33 | Help: help, 34 | PathsSpecial: &logical.Paths{ 35 | Unauthenticated: []string{"login"}, 36 | SealWrapStorage: []string{"config"}, 37 | }, 38 | Paths: framework.PathAppend(NewPathConfig(b), NewPathRole(b), NewPathLogin(b)), 39 | } 40 | 41 | return b 42 | } 43 | 44 | func (b *OpenStackAuthBackend) Close() { 45 | b.clientMutex.Lock() 46 | defer b.clientMutex.Unlock() 47 | 48 | b.client = nil 49 | } 50 | 51 | func (b *OpenStackAuthBackend) getClient(ctx context.Context, s logical.Storage) (*gophercloud.ServiceClient, error) { 52 | b.clientMutex.RLock() 53 | if b.client != nil { 54 | defer b.clientMutex.RUnlock() 55 | return b.client, nil 56 | } 57 | b.clientMutex.RUnlock() 58 | 59 | b.clientMutex.Lock() 60 | defer b.clientMutex.Unlock() 61 | 62 | config, err := readConfig(ctx, s) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | opts := &clientconfig.ClientOpts{ 68 | AuthInfo: &clientconfig.AuthInfo{ 69 | AuthURL: config.AuthURL, 70 | Token: config.Token, 71 | UserID: config.UserID, 72 | Username: config.Username, 73 | Password: config.Password, 74 | ProjectID: config.ProjectID, 75 | ProjectName: config.ProjectName, 76 | UserDomainID: config.UserDomainID, 77 | UserDomainName: config.UserDomainName, 78 | ProjectDomainID: config.ProjectDomainID, 79 | ProjectDomainName: config.ProjectDomainName, 80 | DomainID: config.DomainID, 81 | DomainName: config.DomainName, 82 | }, 83 | } 84 | 85 | if config.TenantID != "" { 86 | opts.AuthInfo.ProjectID = config.TenantID 87 | } 88 | if config.TenantName != "" { 89 | opts.AuthInfo.ProjectName = config.TenantName 90 | } 91 | 92 | authOpts, err := clientconfig.AuthOptions(opts) 93 | if err != nil { 94 | return nil, err 95 | } 96 | authOpts.AllowReauth = true 97 | 98 | provider, err := openstack.AuthenticatedClient(*authOpts) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{}) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | b.client = client 109 | 110 | return b.client, nil 111 | } 112 | 113 | func (b *OpenStackAuthBackend) invalidateHandler(_ context.Context, key string) { 114 | switch key { 115 | case "config": 116 | b.Close() 117 | } 118 | } 119 | 120 | func (b *OpenStackAuthBackend) periodicHandler(ctx context.Context, req *logical.Request) error { 121 | count, err := cleanupAuthAttempt(ctx, req.Storage) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | if count > 0 { 127 | b.Logger().Info(fmt.Sprintf("%d expired auth attempts has been removed", count)) 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 134 | b := NewBackend() 135 | err := b.Setup(ctx, conf) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | return b, nil 141 | } 142 | -------------------------------------------------------------------------------- /plugin/attestor.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/mitchellh/mapstructure" 11 | ) 12 | 13 | type address struct { 14 | Version int `mapstructure:"version"` 15 | Address string `mapstructure:"addr"` 16 | } 17 | 18 | type Attestor struct { 19 | storage logical.Storage 20 | } 21 | 22 | // NewAttestor returns new attestor. 23 | func NewAttestor(s logical.Storage) *Attestor { 24 | return &Attestor{storage: s} 25 | } 26 | 27 | // Attest is used to attest a OpenStack instance based on binded role and IP address. 28 | func (at *Attestor) Attest(instance *servers.Server, role *Role, addr string) error { 29 | deadline, err := at.VerifyAuthPeriod(instance, role.AuthPeriod) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | _, err = at.VerifyAuthLimit(instance, role.AuthLimit, deadline) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | err = at.AttestAddr(instance, addr) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | err = at.AttestStatus(instance) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | err = at.AttestMetadata(instance, role.MetadataKey, role.Name) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = at.AttestTenantID(instance, role.TenantID) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | err = at.AttestUserID(instance, role.UserID) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // AttestMetadata is used to attest a OpenStack instance metadata. 68 | func (at *Attestor) AttestMetadata(instance *servers.Server, metadataKey string, roleName string) error { 69 | val, ok := instance.Metadata[metadataKey] 70 | if !ok { 71 | return errors.New("metadata key not found") 72 | } 73 | 74 | if val != roleName { 75 | return errors.New("metadata role name mismatched") 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // AttestStatus is used to attest the status of OpenStack instance. 82 | func (at *Attestor) AttestStatus(instance *servers.Server) error { 83 | if instance.Status != "ACTIVE" { 84 | return errors.New("instance is not active") 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // AttestAddr is used to attest the IP address of OpenStack instance 91 | // with source IP address. This method support IPv4 only. 92 | func (at *Attestor) AttestAddr(instance *servers.Server, addr string) error { 93 | var addresses map[string][]address 94 | 95 | if instance.AccessIPv4 == addr { 96 | return nil 97 | } 98 | 99 | err := mapstructure.Decode(instance.Addresses, &addresses) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | for _, addrs := range addresses { 105 | for _, val := range addrs { 106 | if val.Version != 4 { 107 | continue 108 | } 109 | 110 | if val.Address == addr { 111 | return nil 112 | } 113 | } 114 | } 115 | 116 | return errors.New("address mismatched") 117 | } 118 | 119 | // AttestTenantID is used to attest the tenant ID of OpenStack instance. 120 | func (at *Attestor) AttestTenantID(instance *servers.Server, tenantID string) error { 121 | if tenantID == "" { 122 | return nil 123 | } 124 | 125 | if instance.TenantID != tenantID { 126 | return errors.New("tenant ID mismatched") 127 | } 128 | 129 | return nil 130 | } 131 | 132 | // AttestUserID is used to attest the user ID of OpenStack instance. 133 | func (at *Attestor) AttestUserID(instance *servers.Server, userID string) error { 134 | if userID == "" { 135 | return nil 136 | } 137 | 138 | if instance.UserID != userID { 139 | return errors.New("user ID mismatched") 140 | } 141 | 142 | return nil 143 | } 144 | 145 | // VerifyAuthPeriod is used to verify the deadline of authentication. 146 | // The deadline is calculated by the create date of OpenStack instance and 147 | // the authentication period specified by a binded role. 148 | func (at *Attestor) VerifyAuthPeriod(instance *servers.Server, period time.Duration) (time.Time, error) { 149 | deadline := instance.Created.Add(period) 150 | if time.Now().After(deadline) { 151 | return deadline, errors.New("authentication deadline exceeded") 152 | } 153 | 154 | return deadline, nil 155 | } 156 | 157 | // VerifyAuthLimit is used to verify the number of attempts of authentication. 158 | // The limit of authentication is specified by a binded role. 159 | func (at *Attestor) VerifyAuthLimit(instance *servers.Server, limit int, deadline time.Time) (int, error) { 160 | ctx := context.Background() 161 | 162 | attempt, err := readAuthAttempt(ctx, at.storage, instance.ID) 163 | if err != nil { 164 | return 0, err 165 | } 166 | 167 | if attempt == nil { 168 | attempt = &AuthAttempt{ 169 | Name: instance.ID, 170 | Deadline: deadline, 171 | Count: 0, 172 | } 173 | } 174 | 175 | attempt.Count = attempt.Count + 1 176 | 177 | err = updateAuthAttempt(ctx, at.storage, attempt) 178 | if err != nil { 179 | return attempt.Count, err 180 | } 181 | 182 | if attempt.Count > limit { 183 | return attempt.Count, errors.New("too many authentication failures") 184 | } 185 | 186 | return attempt.Count, nil 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vault Plugin: OpenStack Auth Backend 2 | 3 | This is a standalone backend plugin for use with Hashicorp Vault. This plugin allows for OpenStack instances to authenticate with Vault. 4 | 5 | ## Getting Started 6 | 7 | This is a [Vault plugin](https://www.vaultproject.io/docs/internals/plugins.html) and is meant to work with Vault. This guide assumes you have already installed Vault and have a basic understanding of how Vault works. 8 | 9 | To learn specifically about how plugins work, see documentation on [Vault plugins](https://www.vaultproject.io/docs/internals/plugins.html). 10 | 11 | ## Setup 12 | 13 | Download the latest plugin binary from the [Releases](https://github.com/summerwind/vault-plugin-auth-openstack/releases) page on GitHub and move the plugin binary into Vault's configured *plugin_directory*. 14 | 15 | ``` 16 | $ mv vault-plugin-auth-openstack /etc/vault/plugins/vault-plugin-auth-openstack 17 | ``` 18 | 19 | Calculate the checksum of the plugin and register it in Vault's plugin catalog. It is highly recommended that you use the published checksums on the Release page to verify integrity. 20 | 21 | ``` 22 | $ export SHA256_SUM=$(shasum -a 256 "/etc/vault/plugins/vault-plugin-auth-openstack" | cut -d' ' -f1) 23 | $ vault write sys/plugins/catalog/auth/openstack \ 24 | command="vault-plugin-auth-openstack" \ 25 | sha_256="${SHA256_SUM}" 26 | ``` 27 | 28 | Enable authentication with the plugin. 29 | 30 | ``` 31 | $ vault auth enable -path="openstack" -plugin-name="openstack" plugin 32 | ``` 33 | 34 | ## Configuration 35 | 36 | In order to authenticate with OpenStack instance, the administrator needs to configure the OpenStack account information and create the role associated with the instance. 37 | 38 | Configure the OpenStack account information that is used to attest an instance with OpenStack API. The OpenStack account used here must have permission to read the instance information. 39 | 40 | ``` 41 | $ vault write auth/openstack/config \ 42 | auth_url="${OS_AUTH_URL}" \ 43 | tenant_name="${OS_TENANT_NAME}" \ 44 | username="${OS_USERNAME}" \ 45 | password="${OS_PASSWORD}" 46 | ``` 47 | 48 | Create a role to associate the OpenStack instance with the Vault policies. The following example creates a role named "dev" associated with the vault policy "prod" and "dev". This example role is identified by the vault-role key contained in Metadata of the OpenStack instance, and up to 3 times of authentication can be attempted in 120 seconds after instance is created. 49 | 50 | ``` 51 | $ vault write auth/openstack/role/dev \ 52 | policies="prod,dev" \ 53 | metadata_key="vault-role" \ 54 | auth_period=120 \ 55 | auth_limit=3 56 | ``` 57 | 58 | ## Usage 59 | 60 | OpenStack instances that use Vault authentication must be created with the metadata key specified in the role. 61 | 62 | ``` 63 | $ openstack server create \ 64 | --flavor ${FLAVOR_NAME} \ 65 | --image ${IMAGE_NAME} \ 66 | --key-name ${KEY_NAME} \ 67 | --property vault-role=dev \ 68 | ${INSTANCE_NAME} 69 | ``` 70 | 71 | After created, instance can be authenticated with Vault as follows. Note that the instance ID must be obtained from config drive or OpenStack metadata server. 72 | 73 | ``` 74 | $ vault write auth/openstack/login instance_id="${INSTANCE_ID}" role="dev" 75 | ``` 76 | 77 | ## Authentication flow 78 | 79 | This plugin gets the instance information from the OpenStack API and attestates the existence of the instance based on the information. The detailed authentication flow is as follows. 80 | 81 | 1. Receive the instance ID and the role name through the `vault login` command. 82 | 2. Get the instance information from OpenStack API based on the instance ID. If the instance information does not exist, the authentication fails. 83 | 3. Get the role configuration based on the role name. If the role configuration does not exist, the authenticate fails. 84 | 4. Validate the authentication period specified in the role with the creation time of the instance. If the deadline was exceeded, the authentication fails. 85 | 5. Validate the limit of authentication attempt count specified in the role. If authentication exceeds the maximum number of attempts, the authentication fails. 86 | 6. Validate the instance IP address with the remote IP address of `vault login`. If address mismatched, the authentication fails. 87 | 7. Validate the status of the instance. If the instance is not active, the authentication fails. 88 | 8. Validate the role name contained in the metadata of the instance with the key specified in the role configuration. If the key of metadata does not exist or role name is mismatched, the authentication fails. 89 | 9. Validate the tenant ID of the instance with the role configuration. If the tenand ID is mismatched, the authentication fails. This validation is performed only if the tenant ID is specified in the role configuration. 90 | 9. Validate the user ID of the instance with the role configuration. If the user ID is mismatched, the authentication fails. This validation is performed only if the user ID is specified in the role configuration. 91 | 92 | ## Development 93 | 94 | If you wish to work on this plugin, you'll first need [Go](https://golang.org) and [go-task](https://github.com/go-task/task) installed on your machine. 95 | 96 | To build a development version of this plugin, run `task build`. This will put the plugin binary in the current directory. 97 | 98 | ``` 99 | $ task build 100 | ``` 101 | 102 | To run the tests, invoke `task test`. 103 | 104 | ``` 105 | $ task test 106 | ``` 107 | 108 | You can also see the test coverage report as follows. 109 | 110 | ``` 111 | $ task cover 112 | ``` 113 | 114 | ## Should I Use This? 115 | 116 | This is an experimental plugin. We don't reccomend to use this in your production. 117 | -------------------------------------------------------------------------------- /plugin/path_login.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/helper/policyutil" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | const loginSynopsis = "Authenticates OpenStack instance with Vault." 14 | const loginDescription = ` 15 | Authenticates OpenStack instance. 16 | ` 17 | 18 | var loginFields map[string]*framework.FieldSchema = map[string]*framework.FieldSchema{ 19 | "instance_id": { 20 | Type: framework.TypeString, 21 | Description: "ID of the instance.", 22 | }, 23 | "role": { 24 | Type: framework.TypeString, 25 | Description: "Name of the role.", 26 | }, 27 | } 28 | 29 | func NewPathLogin(b *OpenStackAuthBackend) []*framework.Path { 30 | return []*framework.Path{ 31 | { 32 | Pattern: "login$", 33 | Fields: loginFields, 34 | Callbacks: map[logical.Operation]framework.OperationFunc{ 35 | logical.UpdateOperation: b.loginHandler, 36 | logical.AliasLookaheadOperation: b.loginHandler, 37 | }, 38 | HelpSynopsis: loginSynopsis, 39 | HelpDescription: loginDescription, 40 | }, 41 | } 42 | } 43 | 44 | func (b *OpenStackAuthBackend) loginHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 45 | var val interface{} 46 | var ok bool 47 | 48 | val, ok = data.GetOk("instance_id") 49 | if !ok { 50 | return logical.ErrorResponse("instance_id required"), nil 51 | } 52 | instanceID := val.(string) 53 | 54 | val, ok = data.GetOk("role") 55 | if !ok { 56 | return logical.ErrorResponse("role required"), nil 57 | } 58 | roleName := val.(string) 59 | 60 | b.Logger().Info("login attempt", "instance_id", instanceID, "role", roleName) 61 | 62 | role, err := readRole(ctx, req.Storage, roleName) 63 | if err != nil || role == nil { 64 | return logical.ErrorResponse(fmt.Sprintf("invalid role: %v", err)), nil 65 | } 66 | 67 | client, err := b.getClient(ctx, req.Storage) 68 | if err != nil { 69 | msg := "openstack client error" 70 | b.Logger().Error(msg, "error", err) 71 | return nil, fmt.Errorf("%s: %v", msg, err) 72 | } 73 | 74 | instance, err := servers.Get(client, instanceID).Extract() 75 | if err != nil { 76 | return logical.ErrorResponse(fmt.Sprintf("failed to find instance: %v", err)), nil 77 | } 78 | 79 | attestor := NewAttestor(req.Storage) 80 | if err != nil { 81 | msg := "attestor error" 82 | b.Logger().Error(msg, "error", err) 83 | return nil, fmt.Errorf("%s: %v", msg, err) 84 | } 85 | 86 | err = attestor.Attest(instance, role, req.Connection.RemoteAddr) 87 | if err != nil { 88 | b.Logger().Info("attestation failed", "error", err) 89 | return logical.ErrorResponse(fmt.Sprintf("failed to login: %v", err)), nil 90 | } 91 | 92 | res := &logical.Response{} 93 | 94 | if req.Operation == logical.AliasLookaheadOperation { 95 | res.Auth = &logical.Auth{ 96 | Alias: &logical.Alias{ 97 | Name: instance.ID, 98 | }, 99 | } 100 | } 101 | 102 | res.Auth = &logical.Auth{ 103 | Period: role.Period, 104 | Alias: &logical.Alias{ 105 | Name: instance.ID, 106 | }, 107 | Policies: role.Policies, 108 | Metadata: map[string]string{ 109 | "role": roleName, 110 | }, 111 | DisplayName: instance.Name, 112 | LeaseOptions: logical.LeaseOptions{ 113 | Renewable: true, 114 | TTL: role.TTL, 115 | MaxTTL: role.MaxTTL, 116 | }, 117 | } 118 | 119 | return res, nil 120 | } 121 | 122 | func (b *OpenStackAuthBackend) authRenewHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 123 | if req.Auth.Alias == nil { 124 | return logical.ErrorResponse("instance ID associated with token is invalid"), nil 125 | } 126 | 127 | instanceID := req.Auth.Alias.Name 128 | if instanceID == "" { 129 | return logical.ErrorResponse("instance ID associated with token is invalid"), nil 130 | } 131 | 132 | roleName := req.Auth.Metadata["role"] 133 | if roleName == "" { 134 | return logical.ErrorResponse("role name associated with token is invalid"), nil 135 | } 136 | 137 | role, err := readRole(ctx, req.Storage, roleName) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | if role == nil { 143 | return logical.ErrorResponse(fmt.Sprintf("role '%s' no longer exists", roleName)), nil 144 | } 145 | 146 | if !policyutil.EquivalentPolicies(role.Policies, req.Auth.Policies) { 147 | return logical.ErrorResponse(fmt.Sprintf("policies on role '%s' have changed, cannot renew", roleName)), nil 148 | } 149 | 150 | client, err := b.getClient(ctx, req.Storage) 151 | if err != nil { 152 | msg := "openstack client error" 153 | b.Logger().Error(msg, "error", err) 154 | return nil, fmt.Errorf("%s: %v", msg, err) 155 | } 156 | 157 | instance, err := servers.Get(client, instanceID).Extract() 158 | if err != nil { 159 | return logical.ErrorResponse(fmt.Sprintf("failed to find instance: %v", err)), nil 160 | } 161 | 162 | attestor := NewAttestor(req.Storage) 163 | if err != nil { 164 | msg := "attestor error" 165 | b.Logger().Error(msg, "error", err) 166 | return nil, fmt.Errorf("%s: %v", msg, err) 167 | } 168 | 169 | err = attestor.AttestMetadata(instance, role.MetadataKey, role.Name) 170 | if err != nil { 171 | return logical.ErrorResponse(fmt.Sprintf("failed to renew: %v", err)), nil 172 | } 173 | 174 | err = attestor.AttestAddr(instance, req.Connection.RemoteAddr) 175 | if err != nil { 176 | return logical.ErrorResponse(fmt.Sprintf("failed to renew: %v", err)), nil 177 | } 178 | 179 | res := &logical.Response{Auth: req.Auth} 180 | res.Auth.Period = role.Period 181 | res.Auth.TTL = role.TTL 182 | res.Auth.MaxTTL = role.MaxTTL 183 | 184 | return res, nil 185 | } 186 | -------------------------------------------------------------------------------- /plugin/path_config.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/vault/sdk/framework" 7 | "github.com/hashicorp/vault/sdk/logical" 8 | ) 9 | 10 | const configSynopsis = "Configures the OpenStack API information." 11 | const configDescription = ` 12 | The OpenStack Auth backend validates the instance infromation and verifies 13 | their existence with the OpenStack API. This endpoint configures the 14 | information to access the OpenStack API. 15 | ` 16 | 17 | var configFields map[string]*framework.FieldSchema = map[string]*framework.FieldSchema{ 18 | "auth_url": { 19 | Type: framework.TypeString, 20 | Description: "Keystone endpoint URL.", 21 | }, 22 | "token": { 23 | Type: framework.TypeString, 24 | Description: "Pre-generated authentication token.", 25 | }, 26 | "user_id": { 27 | Type: framework.TypeString, 28 | Description: "Unique ID of the user.", 29 | }, 30 | "username": { 31 | Type: framework.TypeString, 32 | Description: "Uername of the user.", 33 | }, 34 | "password": { 35 | Type: framework.TypeString, 36 | Description: "The password of the user.", 37 | }, 38 | "project_id": { 39 | Type: framework.TypeString, 40 | Description: "Unique ID of the project.", 41 | }, 42 | "project_name": { 43 | Type: framework.TypeString, 44 | Description: "Human-readable name of the project.", 45 | }, 46 | "tenant_id": { 47 | Type: framework.TypeString, 48 | Description: "Unique ID of the tenant.", 49 | }, 50 | "tenant_name": { 51 | Type: framework.TypeString, 52 | Description: "Human-readable name of the tenant.", 53 | }, 54 | "user_domain_id": { 55 | Type: framework.TypeString, 56 | Description: "Name of the domain where a user resides.", 57 | }, 58 | "user_domain_name": { 59 | Type: framework.TypeString, 60 | Description: "Unique ID of the domain where a user resides.", 61 | }, 62 | "project_domain_id": { 63 | Type: framework.TypeString, 64 | Description: "Unique ID of the domain where a project resides.", 65 | }, 66 | "project_domain_name": { 67 | Type: framework.TypeString, 68 | Description: "Name of the domain where a project resides.", 69 | }, 70 | "domain_id": { 71 | Type: framework.TypeString, 72 | Description: "Unique ID of a domain which can be used to identify the source domain of either a user or a project.", 73 | }, 74 | "domain_name": { 75 | Type: framework.TypeString, 76 | Description: "Name of a domain which can be used to identify the source domain of either a user or a project.", 77 | }, 78 | } 79 | 80 | func NewPathConfig(b *OpenStackAuthBackend) []*framework.Path { 81 | return []*framework.Path{ 82 | &framework.Path{ 83 | Pattern: "config", 84 | Fields: configFields, 85 | Callbacks: map[logical.Operation]framework.OperationFunc{ 86 | logical.CreateOperation: b.updateConfigHandler, 87 | logical.ReadOperation: b.readConfigHandler, 88 | logical.UpdateOperation: b.updateConfigHandler, 89 | }, 90 | HelpSynopsis: configSynopsis, 91 | HelpDescription: configDescription, 92 | }, 93 | } 94 | } 95 | 96 | func (b *OpenStackAuthBackend) readConfigHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 97 | config, err := readConfig(ctx, req.Storage) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | if config == nil { 103 | return nil, nil 104 | } 105 | 106 | res := &logical.Response{ 107 | Data: map[string]interface{}{ 108 | "auth_url": config.AuthURL, 109 | "user_id": config.UserID, 110 | "username": config.Username, 111 | "project_id": config.ProjectID, 112 | "project_name": config.ProjectName, 113 | "tenant_id": config.TenantID, 114 | "tenant_name": config.TenantName, 115 | "user_domain_id": config.UserDomainID, 116 | "user_domain_name": config.UserDomainName, 117 | "project_domain_id": config.ProjectDomainID, 118 | "project_domain_name": config.ProjectDomainName, 119 | "domain_id": config.DomainID, 120 | "domain_name": config.DomainName, 121 | }, 122 | } 123 | 124 | return res, nil 125 | } 126 | 127 | func (b *OpenStackAuthBackend) updateConfigHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 128 | var val interface{} 129 | var ok bool 130 | 131 | config, err := readConfig(ctx, req.Storage) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | if config == nil { 137 | config = &Config{} 138 | } 139 | 140 | val, ok = data.GetOk("auth_url") 141 | if ok { 142 | config.AuthURL = val.(string) 143 | } 144 | 145 | val, ok = data.GetOk("token") 146 | if ok { 147 | config.Token = val.(string) 148 | } 149 | 150 | val, ok = data.GetOk("user_id") 151 | if ok { 152 | config.UserID = val.(string) 153 | } 154 | 155 | val, ok = data.GetOk("username") 156 | if ok { 157 | config.Username = val.(string) 158 | } 159 | 160 | val, ok = data.GetOk("password") 161 | if ok { 162 | config.Password = val.(string) 163 | } 164 | 165 | val, ok = data.GetOk("project_id") 166 | if ok { 167 | config.ProjectID = val.(string) 168 | } 169 | 170 | val, ok = data.GetOk("project_name") 171 | if ok { 172 | config.ProjectName = val.(string) 173 | } 174 | 175 | val, ok = data.GetOk("tenant_id") 176 | if ok { 177 | config.TenantID = val.(string) 178 | } 179 | 180 | val, ok = data.GetOk("tenant_name") 181 | if ok { 182 | config.TenantName = val.(string) 183 | } 184 | 185 | val, ok = data.GetOk("user_domain_id") 186 | if ok { 187 | config.UserDomainID = val.(string) 188 | } 189 | 190 | val, ok = data.GetOk("user_domain_name") 191 | if ok { 192 | config.UserDomainName = val.(string) 193 | } 194 | 195 | val, ok = data.GetOk("project_domain_id") 196 | if ok { 197 | config.ProjectDomainID = val.(string) 198 | } 199 | 200 | val, ok = data.GetOk("project_domain_name") 201 | if ok { 202 | config.ProjectDomainName = val.(string) 203 | } 204 | 205 | val, ok = data.GetOk("domain_id") 206 | if ok { 207 | config.DomainID = val.(string) 208 | } 209 | 210 | val, ok = data.GetOk("domain_name") 211 | if ok { 212 | config.DomainName = val.(string) 213 | } 214 | 215 | entry, err := logical.StorageEntryJSON("config", config) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | err = req.Storage.Put(ctx, entry) 221 | if err != nil { 222 | return nil, err 223 | } 224 | 225 | b.Close() 226 | 227 | return nil, nil 228 | } 229 | -------------------------------------------------------------------------------- /plugin/path_role.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/hashicorp/vault/sdk/helper/policyutil" 11 | "github.com/hashicorp/vault/sdk/logical" 12 | ) 13 | 14 | const roleSynopsis = "Register an role with the backend." 15 | const roleDescription = ` 16 | A role is required to authenticate with this backend. The role binds 17 | OpenStack instance with token policies and token settings. The bindings, 18 | token polices and token settings can all be configured using this endpoint. 19 | ` 20 | 21 | const roleListSynopsis = "Lists all the roles registered with the backend." 22 | const roleListDescription = ` 23 | The list will contain the names of the roles. 24 | ` 25 | 26 | var roleFields map[string]*framework.FieldSchema = map[string]*framework.FieldSchema{ 27 | "name": { 28 | Type: framework.TypeString, 29 | Description: "Name of the role.", 30 | }, 31 | "policies": { 32 | Type: framework.TypeCommaStringSlice, 33 | Description: "Policies to be set on tokens issued using this role.", 34 | }, 35 | "ttl": { 36 | Type: framework.TypeDurationSecond, 37 | Default: 0, 38 | Description: "Duration in seconds after which the issued token should expire. Defaults to 0, in which case the value will fallback to the system/mount defaults.", 39 | }, 40 | "max_ttl": { 41 | Type: framework.TypeDurationSecond, 42 | Default: 0, 43 | Description: "The maximum allowed lifetime of tokens issued using this role.", 44 | }, 45 | "period": { 46 | Type: framework.TypeDurationSecond, 47 | Default: 0, 48 | Description: "If set, indicates that the token generated using this role should never expire. The token should be renewed within the duration specified by this value. At each renewal, the token's TTL will be set to the value of this parameter.", 49 | }, 50 | "metadata_key": { 51 | Type: framework.TypeString, 52 | Default: "vault-role", 53 | Description: "The key name of the instance metadata to validate the role specified during authentication. The role name must be specified for the key of metadata of the instance specified here.", 54 | }, 55 | "auth_period": { 56 | Type: framework.TypeDurationSecond, 57 | Default: 120, 58 | Description: "The authentication deadline. This is the relative number of seconds since the instance started.", 59 | }, 60 | "auth_limit": { 61 | Type: framework.TypeInt, 62 | Default: 1, 63 | Description: "The number of times an instance can try authentication.", 64 | }, 65 | } 66 | 67 | func NewPathRole(b *OpenStackAuthBackend) []*framework.Path { 68 | return []*framework.Path{ 69 | { 70 | Pattern: fmt.Sprintf("role/%s", framework.GenericNameRegex("name")), 71 | Fields: roleFields, 72 | ExistenceCheck: b.checkRoleHandler, 73 | Callbacks: map[logical.Operation]framework.OperationFunc{ 74 | logical.CreateOperation: b.updateRoleHandler, 75 | logical.ReadOperation: b.readRoleHandler, 76 | logical.UpdateOperation: b.updateRoleHandler, 77 | logical.DeleteOperation: b.deleteRoleHandler, 78 | }, 79 | HelpSynopsis: roleSynopsis, 80 | HelpDescription: roleDescription, 81 | }, 82 | { 83 | Pattern: "role/?", 84 | Callbacks: map[logical.Operation]framework.OperationFunc{ 85 | logical.ListOperation: b.listRoleHandler, 86 | }, 87 | HelpSynopsis: roleListSynopsis, 88 | HelpDescription: roleListDescription, 89 | }, 90 | { 91 | Pattern: "roles/?", 92 | Callbacks: map[logical.Operation]framework.OperationFunc{ 93 | logical.ListOperation: b.listRoleHandler, 94 | }, 95 | HelpSynopsis: roleListSynopsis, 96 | HelpDescription: roleListDescription, 97 | }, 98 | } 99 | } 100 | 101 | func (b *OpenStackAuthBackend) checkRoleHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 102 | roleName := strings.ToLower(data.Get("name").(string)) 103 | entry, err := readRole(ctx, req.Storage, roleName) 104 | return (entry != nil), err 105 | } 106 | 107 | func (b *OpenStackAuthBackend) readRoleHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 108 | roleName := strings.ToLower(data.Get("name").(string)) 109 | if roleName == "" { 110 | return logical.ErrorResponse("role name is required"), nil 111 | } 112 | 113 | role, err := readRole(ctx, req.Storage, roleName) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | if role == nil { 119 | return nil, nil 120 | } 121 | 122 | res := &logical.Response{ 123 | Data: map[string]interface{}{ 124 | "policies": role.Policies, 125 | "ttl": int64(role.TTL / time.Second), 126 | "max_ttl": int64(role.MaxTTL / time.Second), 127 | "period": int64(role.Period / time.Second), 128 | "metadata_key": role.MetadataKey, 129 | "auth_period": int64(role.AuthPeriod / time.Second), 130 | "auth_limit": role.AuthLimit, 131 | }, 132 | } 133 | 134 | return res, nil 135 | } 136 | 137 | func (b *OpenStackAuthBackend) updateRoleHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 138 | var val interface{} 139 | var ok bool 140 | 141 | roleName := strings.ToLower(data.Get("name").(string)) 142 | if roleName == "" { 143 | return logical.ErrorResponse("role name is required"), nil 144 | } 145 | 146 | role, err := readRole(ctx, req.Storage, roleName) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | if role == nil { 152 | role = &Role{Name: roleName} 153 | } 154 | 155 | val, ok = data.GetOk("policies") 156 | if ok { 157 | role.Policies = policyutil.ParsePolicies(val) 158 | } 159 | 160 | val, ok = data.GetOk("ttl") 161 | if ok { 162 | role.TTL = time.Duration(val.(int)) * time.Second 163 | } 164 | 165 | val, ok = data.GetOk("max_ttl") 166 | if ok { 167 | role.MaxTTL = time.Duration(val.(int)) * time.Second 168 | } 169 | 170 | val, ok = data.GetOk("period") 171 | if ok { 172 | role.Period = time.Duration(val.(int)) * time.Second 173 | } 174 | 175 | val, ok = data.GetOk("metadata_key") 176 | if ok { 177 | role.MetadataKey = val.(string) 178 | } 179 | 180 | val, ok = data.GetOk("auth_period") 181 | if ok { 182 | role.AuthPeriod = time.Duration(val.(int)) * time.Second 183 | } 184 | 185 | val, ok = data.GetOk("auth_limit") 186 | if ok { 187 | role.AuthLimit = val.(int) 188 | } 189 | 190 | warnings, err := role.Validate(b.System()) 191 | if err != nil { 192 | return logical.ErrorResponse(fmt.Sprintf("invalid role: %v", err)), nil 193 | } 194 | 195 | entry, err := logical.StorageEntryJSON(fmt.Sprintf("role/%s", roleName), role) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | err = req.Storage.Put(ctx, entry) 201 | if err != nil { 202 | return nil, err 203 | } 204 | 205 | res := &logical.Response{ 206 | Warnings: warnings, 207 | } 208 | 209 | return res, nil 210 | } 211 | 212 | func (b *OpenStackAuthBackend) deleteRoleHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 213 | roleName := strings.ToLower(data.Get("name").(string)) 214 | if roleName == "" { 215 | return logical.ErrorResponse("role name is required"), nil 216 | } 217 | 218 | err := req.Storage.Delete(ctx, fmt.Sprintf("role/%s", roleName)) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | return nil, nil 224 | } 225 | 226 | func (b *OpenStackAuthBackend) listRoleHandler(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 227 | roles, err := req.Storage.List(ctx, "role/") 228 | if err != nil { 229 | return nil, err 230 | } 231 | 232 | return logical.ListResponse(roles), nil 233 | } 234 | -------------------------------------------------------------------------------- /plugin/attestor_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" 9 | ) 10 | 11 | func newTestInstance() *servers.Server { 12 | return &servers.Server{ 13 | ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", 14 | Name: "test", 15 | UserID: "9349aff8be7545ac9d2f1d00999a23cd", 16 | TenantID: "fcad67a6189847c4aecfa3c81a05783b", 17 | HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", 18 | Status: "ACTIVE", 19 | AccessIPv4: "", 20 | Addresses: map[string]interface{}{}, 21 | Metadata: map[string]string{}, 22 | Created: time.Now(), 23 | Updated: time.Now(), 24 | } 25 | } 26 | 27 | func TestAttest(t *testing.T) { 28 | var tests = []struct { 29 | diff int 30 | limit int 31 | attempt int 32 | metadata string 33 | status string 34 | addr string 35 | tenantID string 36 | result bool 37 | }{ 38 | {0, 2, 1, "test", "ACTIVE", "192.168.1.1", "fcad67a6189847c4aecfa3c81a05783b", true}, 39 | {-130, 2, 1, "test", "ACTIVE", "192.168.1.1", "fcad67a6189847c4aecfa3c81a05783b", false}, 40 | {0, 2, 3, "test", "ACTIVE", "192.168.1.1", "fcad67a6189847c4aecfa3c81a05783b", false}, 41 | {0, 2, 1, "invalid", "ACTIVE", "192.168.1.1", "fcad67a6189847c4aecfa3c81a05783b", false}, 42 | {0, 2, 1, "test", "ERROR", "192.168.1.1", "fcad67a6189847c4aecfa3c81a05783b", false}, 43 | {0, 2, 1, "test", "ACTIVE", "192.168.1.2", "fcad67a6189847c4aecfa3c81a05783b", false}, 44 | {0, 2, 1, "test", "ACTIVE", "192.168.1.1", "invalid", false}, 45 | } 46 | 47 | _, storage := newTestBackend(t) 48 | attestor := NewAttestor(storage) 49 | 50 | role := &Role{ 51 | Name: "test", 52 | Policies: []string{"test"}, 53 | TTL: time.Duration(60) * time.Second, 54 | MaxTTL: time.Duration(120) * time.Second, 55 | Period: time.Duration(120) * time.Second, 56 | MetadataKey: "vault-role", 57 | TenantID: "fcad67a6189847c4aecfa3c81a05783b", 58 | AuthPeriod: time.Duration(120) * time.Second, 59 | AuthLimit: 2, 60 | } 61 | 62 | for i, test := range tests { 63 | var err error 64 | 65 | instance := newTestInstance() 66 | instance.ID = fmt.Sprintf("test%d", i) 67 | instance.AccessIPv4 = test.addr 68 | instance.Metadata["vault-role"] = test.metadata 69 | instance.Status = test.status 70 | instance.TenantID = test.tenantID 71 | instance.Created = time.Now().Add(time.Duration(test.diff) * time.Second) 72 | 73 | for i := 0; i < test.attempt; i++ { 74 | err = attestor.Attest(instance, role, "192.168.1.1") 75 | } 76 | if (err == nil) != test.result { 77 | t.Errorf("unexpected result: %v - %v", test, err) 78 | } 79 | } 80 | } 81 | 82 | func TestAttestMetadata(t *testing.T) { 83 | var tests = []struct { 84 | key string 85 | val string 86 | result bool 87 | }{ 88 | {"vault-role", "test", true}, 89 | {"invalid", "test", false}, 90 | {"vault-role", "invalid", false}, 91 | } 92 | 93 | _, storage := newTestBackend(t) 94 | attestor := NewAttestor(storage) 95 | 96 | for _, test := range tests { 97 | instance := newTestInstance() 98 | instance.Metadata[test.key] = test.val 99 | 100 | err := attestor.AttestMetadata(instance, "vault-role", "test") 101 | if (err == nil) != test.result { 102 | t.Errorf("unexpected result: %v - %v", test, err) 103 | } 104 | } 105 | } 106 | 107 | func TestAttestStatus(t *testing.T) { 108 | var tests = []struct { 109 | status string 110 | result bool 111 | }{ 112 | {"ACTIVE", true}, 113 | {"STOPPED", false}, 114 | } 115 | 116 | _, storage := newTestBackend(t) 117 | attestor := NewAttestor(storage) 118 | 119 | for _, test := range tests { 120 | instance := newTestInstance() 121 | instance.Status = test.status 122 | 123 | err := attestor.AttestStatus(instance) 124 | if (err == nil) != test.result { 125 | t.Errorf("unexpected result: %v - %v", test, err) 126 | } 127 | } 128 | } 129 | 130 | func TestAttestAddr(t *testing.T) { 131 | var tests = []struct { 132 | access string 133 | addresses []string 134 | result bool 135 | }{ 136 | {"192.168.1.1", []string{"192.168.1.1"}, true}, 137 | {"192.168.1.1", []string{}, true}, 138 | {"", []string{"192.168.1.1"}, true}, 139 | {"", []string{"192.168.1.1", "192.168.1.2"}, true}, 140 | {"192.168.1.2", []string{"192.168.1.2"}, false}, 141 | {"192.168.1.2", []string{}, false}, 142 | {"", []string{"192.168.1.2"}, false}, 143 | {"", []string{"192.168.1.2", "192.168.1.3"}, false}, 144 | } 145 | 146 | _, storage := newTestBackend(t) 147 | attestor := NewAttestor(storage) 148 | 149 | for _, test := range tests { 150 | instance := newTestInstance() 151 | instance.AccessIPv4 = test.access 152 | if len(test.addresses) > 0 { 153 | addresses := []interface{}{} 154 | for _, addr := range test.addresses { 155 | addresses = append(addresses, map[string]interface{}{ 156 | "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", 157 | "OS-EXT-IPS:type": "fixed", 158 | "version": float64(4), 159 | "addr": addr, 160 | }) 161 | } 162 | instance.Addresses = map[string]interface{}{ 163 | "private": addresses, 164 | } 165 | } 166 | 167 | err := attestor.AttestAddr(instance, "192.168.1.1") 168 | if (err == nil) != test.result { 169 | t.Errorf("unexpected result: %v - %v", test, err) 170 | } 171 | } 172 | } 173 | 174 | func TestAttestTenantID(t *testing.T) { 175 | var tests = []struct { 176 | tenantID string 177 | result bool 178 | }{ 179 | {"", true}, 180 | {"fcad67a6189847c4aecfa3c81a05783b", true}, 181 | {"invalid", false}, 182 | } 183 | 184 | _, storage := newTestBackend(t) 185 | attestor := NewAttestor(storage) 186 | 187 | for _, test := range tests { 188 | instance := newTestInstance() 189 | 190 | err := attestor.AttestTenantID(instance, test.tenantID) 191 | if (err == nil) != test.result { 192 | t.Errorf("unexpected result: %v - %v", test, err) 193 | } 194 | } 195 | } 196 | 197 | func TestAttestUserID(t *testing.T) { 198 | var tests = []struct { 199 | userID string 200 | result bool 201 | }{ 202 | {"", true}, 203 | {"9349aff8be7545ac9d2f1d00999a23cd", true}, 204 | {"invalid", false}, 205 | } 206 | 207 | _, storage := newTestBackend(t) 208 | attestor := NewAttestor(storage) 209 | 210 | for _, test := range tests { 211 | instance := newTestInstance() 212 | 213 | err := attestor.AttestUserID(instance, test.userID) 214 | if (err == nil) != test.result { 215 | t.Errorf("unexpected result: %v - %v", test, err) 216 | } 217 | } 218 | } 219 | 220 | func TestVerifyAuthPeriod(t *testing.T) { 221 | var tests = []struct { 222 | diff int 223 | period int 224 | result bool 225 | }{ 226 | {0, 120, true}, 227 | {-119, 120, true}, 228 | {-120, 120, false}, 229 | {-121, 120, false}, 230 | } 231 | 232 | _, storage := newTestBackend(t) 233 | attestor := NewAttestor(storage) 234 | 235 | for _, test := range tests { 236 | instance := newTestInstance() 237 | instance.Created = time.Now().Add(time.Duration(test.diff) * time.Second) 238 | period := time.Duration(test.period) * time.Second 239 | 240 | _, err := attestor.VerifyAuthPeriod(instance, period) 241 | if (err == nil) != test.result { 242 | t.Errorf("unexpected result: %v - %v", test, err) 243 | } 244 | } 245 | } 246 | 247 | func TestVerifyAuthLimit(t *testing.T) { 248 | instance := newTestInstance() 249 | limit := 2 250 | deadline := time.Now().Add(30 * time.Second) 251 | 252 | _, storage := newTestBackend(t) 253 | attestor := NewAttestor(storage) 254 | 255 | count, err := attestor.VerifyAuthLimit(instance, limit, deadline) 256 | if count != 1 || err != nil { 257 | t.Errorf("unexpected result: [%d] %v", count, err) 258 | } 259 | 260 | count, err = attestor.VerifyAuthLimit(instance, limit, deadline) 261 | if count != 2 || err != nil { 262 | t.Errorf("unexpected result: [%d] %v", count, err) 263 | } 264 | 265 | count, err = attestor.VerifyAuthLimit(instance, limit, deadline) 266 | if count != 3 || err == nil { 267 | t.Errorf("unexpected result: [%d]", count) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 4 | github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= 5 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= 6 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 9 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 10 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 16 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 17 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 18 | github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 19 | github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= 20 | github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31 h1:28FVBuwkwowZMjbA7M0wXsI6t3PYulRTMio3SO+eKCM= 21 | github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 22 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 24 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 27 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 29 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 30 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 31 | github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM= 32 | github.com/gophercloud/gophercloud v0.8.0 h1:1ylFFLRx7otpfRPSuOm77q8HLSlSOwYCGDeXmXJhX7A= 33 | github.com/gophercloud/gophercloud v0.8.0/go.mod h1:Kc/QKr9thLKruO/dG0szY8kRIYS+iENz0ziI0hJf76A= 34 | github.com/gophercloud/utils v0.0.0-20200204043447-9864b6f1f12f h1:JCE3TtmNKlOUeXXdxLe1ipU7F0GOxcj+BenaG4uiz8Y= 35 | github.com/gophercloud/utils v0.0.0-20200204043447-9864b6f1f12f/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w= 36 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 37 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 38 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 39 | github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= 40 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 41 | github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= 42 | github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 43 | github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 44 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 45 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 46 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 47 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 48 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 49 | github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= 50 | github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= 51 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 52 | github.com/hashicorp/go-retryablehttp v0.6.2 h1:bHM2aVXwBtBJWxHtkSrWuI4umABCUczs52eiUS9nSiw= 53 | github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY= 54 | github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8= 55 | github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 56 | github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= 57 | github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= 58 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 59 | github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= 60 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 61 | github.com/hashicorp/go-uuid v1.0.2-0.20191001231223-f32f5fe8d6a8 h1:PKbxRbsOP7R3f/TpdqcgXrO69T3yd9nLoR+RMRUxSxA= 62 | github.com/hashicorp/go-uuid v1.0.2-0.20191001231223-f32f5fe8d6a8/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 63 | github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= 64 | github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 65 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 66 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 67 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 68 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 69 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 70 | github.com/hashicorp/vault/api v1.0.5-0.20200117231345-460d63e36490 h1:jzQDZKCJV74KjQS3Ag3Aunjd9aUP1whRaNZzXqcEHs4= 71 | github.com/hashicorp/vault/api v1.0.5-0.20200117231345-460d63e36490/go.mod h1:Uf8LaHyrYsgVgHzO2tMZKhqRGlL3UJ6XaSwW2EA1Iqo= 72 | github.com/hashicorp/vault/sdk v0.1.14-0.20191108161836-82f2b5571044/go.mod h1:PcekaFGiPJyHnFy+NZhP6ll650zEw51Ag7g/YEa+EOU= 73 | github.com/hashicorp/vault/sdk v0.1.14-0.20200121232954-73f411823aa0 h1:19FHdWKzeCtqcRA5dmXuv7/EZzkBMJRtcx+u7zsEnTs= 74 | github.com/hashicorp/vault/sdk v0.1.14-0.20200121232954-73f411823aa0/go.mod h1:PcekaFGiPJyHnFy+NZhP6ll650zEw51Ag7g/YEa+EOU= 75 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 76 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= 77 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 78 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 79 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 80 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 81 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 82 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 83 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 84 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 85 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 86 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 87 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 88 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 89 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 90 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 91 | github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= 92 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 93 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 94 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 95 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 96 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 97 | github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 98 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 99 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 100 | github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= 101 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 102 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 103 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 104 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 105 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 106 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 107 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 108 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 109 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 110 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 111 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 112 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 115 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 116 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 117 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 118 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 119 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 120 | golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU= 121 | golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 122 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 123 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 124 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 125 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 126 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 129 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 130 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 131 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 132 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 133 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 134 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= 135 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 136 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 137 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 143 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 147 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= 148 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= 151 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 153 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= 154 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 155 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 156 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 157 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 158 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 159 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 160 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 161 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 162 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 163 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 164 | golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 165 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 168 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 169 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 170 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo= 171 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 172 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 173 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 174 | google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= 175 | google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 176 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 177 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 178 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 179 | gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= 180 | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 181 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 183 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 185 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 186 | --------------------------------------------------------------------------------