View
43
Download
8
Category
Preview:
Citation preview
Copyright © 2017 HashiCorp
Vault Plugin InfrastructureNicolas Corrarello - Solutions Engineering Lead International
HashiCorp
Nicolas CorrarelloSolutions Engineering Lead - InternationalInternational Man of Mystery, licensed to:• Methodology• Field Work• Implementations• Develop• Document• School runs
@nomadic_geek
@nomadic_geek
Vault rocks!!!
•Takes a sensible and programmatic approach to security • It’s Open Source • It’s fast! • It’s a “Security product” (As defined by
INFOSEC)
@nomadic_geek
Problems
A few biggies….
• Vault is written in Go, I’ve never written a single line of Go
• I knew how Vault works, but never payed attention to the internals (See “I don’t know Go”)
• I’m in the US, jet lagged, and in a conference
@nomadic_geek
Advantages
1. I understand how both Vault and Nomad work
2. I know that the Consul backend does something pretty similar
3. I’m motivated
4. I’m in the US, in a conference, next to a bunch of Vault engineers I can ask questions to!
@nomadic_geek
What do I need to accomplish
Nomad Secret Backend
Access / TTL
Vault Logical Storage config/access config/lease lease/* role/*
Token Renew/Revoke
Role Create/Update/Delete
Nomad Client
func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Paths: []*framework.Path{
pathConfigAccess(&b),
pathConfigLease(&b),
pathListRoles(&b),
pathRoles(&b),
pathCredsCreate(&b),
},
Secrets: []*framework.Secret{
secretToken(&b),
},
BackendType: logical.TypeLogical,
}
return &b
}
Backend Type: TypeLogical, TypeCredential
Standard Framework Backend
FrontEnd Paths
Type of Secret Standard Secret Framework
import (
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func (b *backend) client(s logical.Storage) (*api.Client, error) {
conf, err := b.readConfigAccess(s)
if err != nil {
return nil, err
}
nomadConf := api.DefaultConfig()
nomadConf.Address = conf.Address
nomadConf.SecretID = conf.Token
client, err := api.NewClient(nomadConf)
if err != nil {
return nil, err
}
return client, nil
}
I need a client
Client Config
Error Handling
Error handling (lots)
Read Configuration
func pathConfigAccess(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/access",
Fields: map[string]*framework.FieldSchema{ "address": &framework.FieldSchema{ Type: framework.TypeString, Description: "Nomad server address", }, "scheme": &framework.FieldSchema{ Type: framework.TypeString, Description: "URI scheme for the Nomad address", Default: "https", },
"token": &framework.FieldSchema{ Type: framework.TypeString, Description: "Token for API calls", },
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathConfigAccessRead,
logical.UpdateOperation: b.pathConfigAccessWrite,
},
}
}
Need a function that return access configuration
Fields, lots of fields
Store it in API / Logical
Operations
func (b *backend) pathConfigAccessRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
conf, err := b.readConfigAccess(req.Storage)
if err != nil { return nil, err } if conf == nil { return nil, fmt.Errorf("nomad access configuration not found") }
return &logical.Response{ Data: map[string]interface{}{ "address": conf.Address, "scheme": conf.Scheme, }, }, nil }
func (b *backend) pathConfigAccessWrite(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) { entry, err := logical.StorageEntryJSON("config/access", accessConfig{ Address: data.Get("address").(string), Scheme: data.Get("scheme").(string), Token: data.Get("token").(string),
}) if err != nil { return nil, err }
if err := req.Storage.Put(entry); err != nil { return nil, err } return nil, nil
}
Define the function
Read Config
Handle Errors
Return Object
func secretToken(b *backend) *framework.Secret { return &framework.Secret{ Type: SecretTokenType, Fields: map[string]*framework.FieldSchema{ "token": &framework.FieldSchema{ Type: framework.TypeString, Description: "Request token", }, }, Renew: b.secretTokenRenew, Revoke: b.secretTokenRevoke, } }
func (b *backend) secretTokenRenew( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { lease, err := b.LeaseConfig(req.Storage) if err != nil { return nil, err } if lease == nil { lease = &configLease{} } return framework.LeaseExtend(lease.TTL, lease.MaxTTL, b.System())(req, d) }
func (b *backend) secretTokenRevoke( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { c, err := b.client(req.Storage) if err != nil { return nil, err } tokenRaw := req.Secret.InternalData["accessor_id"] _, err = c.ACLTokens().Delete(tokenRaw.(string), nil) if err != nil { return nil, err } return nil, nil }
Define the Secret Token function
Map Functions
Revoke Token
Renew Token
func (b *backend) pathRolesWrite( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { tokenType := d.Get("type").(string) name := d.Get("name").(string) global := d.Get("global").(bool) policy := d.Get("policy").([]string) switch tokenType { case "client": if len(policy) == 0 { return logical.ErrorResponse( "policy cannot be empty when using client tokens"), nil } case "management": if len(policy) != 0 { return logical.ErrorResponse( "policy should be empty when using management tokens"), nil } default: return logical.ErrorResponse( "type must be \"client\" or \"management\""), nil } entry, err := logical.StorageEntryJSON("role/"+name, roleConfig{ Policy: policy, TokenType: tokenType, Global: global, }) if err != nil { return nil, err } if err := req.Storage.Put(entry); err != nil { return nil, err } return nil, nil }
Define the role creation function
Token Attributes
Store the role
Validate parameters
func pathCredsCreate(b *backend) *framework.Path { return &framework.Path{ Pattern: "creds/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ "name": &framework.FieldSchema{ Type: framework.TypeString, Description: "Name of the role", }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.pathTokenRead, }, } }
Define the Credentials Creation Function
Map Functions
func (b *backend) pathTokenRead( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) role, err := b.Role(req.Storage, name) if err != nil { return nil, fmt.Errorf("error retrieving role: %s", err) } if role == nil { return logical.ErrorResponse(fmt.Sprintf("Role '%s' not found", name)), nil } leaseConfig, err := b.LeaseConfig(req.Storage) if err != nil { return nil, err } if leaseConfig == nil { leaseConfig = &configLease{} } c, err := b.client(req.Storage) if err != nil { return nil, err } tokenName := fmt.Sprintf("Vault %s %s %d", name, req.DisplayName, time.Now().UnixNano()) token, _, err := c.ACLTokens().Create(&api.ACLToken{ Name: tokenName, Type: role.TokenType, Policies: role.Policy, Global: role.Global, }, nil) if err != nil { return logical.ErrorResponse(err.Error()), nil } resp := b.Secret(SecretTokenType).Response(map[string]interface{}{ "secret_id": token.SecretID, "accessor_id": token.AccessorID, }, map[string]interface{}{ "accessor_id": token.AccessorID, }) resp.Secret.TTL = leaseConfig.TTL return resp, nil }
Random string for token Name
Get the Role object (for the policy names)
Return accessor and token
But only store accessor
func main() { apiClientMeta := &pluginutil.APIClientMeta{} flags := apiClientMeta.FlagSet() flags.Parse(os.Args[1:]) tlsConfig := apiClientMeta.GetTLSConfig() tlsProviderFunc := pluginutil.VaultPluginTLSProvider(tlsConfig) if err := plugin.Serve(&plugin.ServeOpts{ BackendFactoryFunc: Factory, TLSProviderFunc: tlsProviderFunc, }); err != nil { log.Fatal(err) } }
func Factory(c *logical.BackendConfig) (logical.Backend, error) { b := Backend(c) if err := b.Setup(c); err != nil { return nil, err } return b, nil } type backend struct { *framework.Backend }
An independent process that communicates With Vault using RPC
Using Mutual TLS automatically generated
Create Factory
Define Backend
@nomadic_geek
Lessons learned
• Go, err := ‘awesome’, nil
• So many things in Vault were so seamless that really looked like magic.
• The best kept secret is the one you don’t know about.
• Attach all your functions to the Backend of your plugin (Client, Token, Role).
• If I have a pence for each if err == nil … (Seriously, maybe 30% of my code is error handling).
• The other 60% of my code, is just mapping interfaces existing in Vault.
• The remaining 10%, logic that I wrote.
• GoDoc is your friend.
• You can actually write plugins in different languages (see gRPC)
• Document your stuff (https://github.com/hashicorp/vault/tree/f-nomad/website/source/docs/secrets/nomad).
@nomadic_geek
Reference
• Full Code in the f-nomad branch (soon to be master): https://github.com/hashicorp/vault/tree/f-nomad/builtin/logical/nomad
• Seth Vargo did an awesome post on this (with examples) that cover Auth Plugins:
• https://www.hashicorp.com/blog/building-a-vault-secure-plugin
• https://github.com/sethvargo/vault-auth-slack
• A couple of recommended HashiConf talks:
• https://www.youtube.com/watch?v=bCNSvUrK_BA - Deep dive on Vault AWS Auth backend
• https://www.youtube.com/watch?v=rd0xyT8xMqg - Authenticating to Vault with Google Platform
Recommended