25
Copyright © 2017 HashiCorp Vault Plugin Infrastructure Nicolas Corrarello - Solutions Engineering Lead International HashiCorp

HashiCorp Vault Plugin Infrastructure

Embed Size (px)

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

There is something about Nomad….

What?!?!?!?

@nomadic_geek

Authentication Flow

LDAP AuthVault TokenVault Token

Nomad Token

+ Nomad Token

@nomadic_geek

A great idea is not enough. Execute!

@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

@nomadic_geek

Didn’t you say Plugin?

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

COME JOIN US

HashiCorp.com/jobs

Thank you.

[email protected]