From 23431f42462755495eb75f3090721b02aec0c404 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 30 Sep 2016 11:06:04 -0700 Subject: [PATCH 1/5] Add official Vault client library Also includes no-op upgrades to various pre-existing vendored Vault packages. --- vendor/github.com/hashicorp/vault/api/SPEC.md | 611 ++++++++++++++++++ vendor/github.com/hashicorp/vault/api/auth.go | 11 + .../hashicorp/vault/api/auth_token.go | 223 +++++++ .../github.com/hashicorp/vault/api/client.go | 416 ++++++++++++ vendor/github.com/hashicorp/vault/api/help.go | 25 + .../github.com/hashicorp/vault/api/logical.go | 176 +++++ .../github.com/hashicorp/vault/api/request.go | 71 ++ .../hashicorp/vault/api/response.go | 72 +++ .../github.com/hashicorp/vault/api/secret.go | 68 ++ vendor/github.com/hashicorp/vault/api/ssh.go | 38 ++ .../hashicorp/vault/api/ssh_agent.go | 257 ++++++++ vendor/github.com/hashicorp/vault/api/sys.go | 11 + .../hashicorp/vault/api/sys_audit.go | 114 ++++ .../hashicorp/vault/api/sys_auth.go | 87 +++ .../hashicorp/vault/api/sys_capabilities.go | 43 ++ .../hashicorp/vault/api/sys_generate_root.go | 77 +++ .../hashicorp/vault/api/sys_init.go | 54 ++ .../hashicorp/vault/api/sys_leader.go | 20 + .../hashicorp/vault/api/sys_lease.go | 48 ++ .../hashicorp/vault/api/sys_mounts.go | 142 ++++ .../hashicorp/vault/api/sys_policy.go | 95 +++ .../hashicorp/vault/api/sys_rekey.go | 202 ++++++ .../hashicorp/vault/api/sys_rotate.go | 30 + .../hashicorp/vault/api/sys_seal.go | 59 ++ .../hashicorp/vault/api/sys_stepdown.go | 10 + vendor/github.com/sethgrid/pester/LICENSE.md | 21 + vendor/github.com/sethgrid/pester/README.md | 126 ++++ vendor/github.com/sethgrid/pester/main.go | 423 ++++++++++++ vendor/vendor.json | 18 +- 29 files changed, 3542 insertions(+), 6 deletions(-) create mode 100644 vendor/github.com/hashicorp/vault/api/SPEC.md create mode 100644 vendor/github.com/hashicorp/vault/api/auth.go create mode 100644 vendor/github.com/hashicorp/vault/api/auth_token.go create mode 100644 vendor/github.com/hashicorp/vault/api/client.go create mode 100644 vendor/github.com/hashicorp/vault/api/help.go create mode 100644 vendor/github.com/hashicorp/vault/api/logical.go create mode 100644 vendor/github.com/hashicorp/vault/api/request.go create mode 100644 vendor/github.com/hashicorp/vault/api/response.go create mode 100644 vendor/github.com/hashicorp/vault/api/secret.go create mode 100644 vendor/github.com/hashicorp/vault/api/ssh.go create mode 100644 vendor/github.com/hashicorp/vault/api/ssh_agent.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_audit.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_auth.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_capabilities.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_generate_root.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_init.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_leader.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_lease.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_mounts.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_policy.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_rekey.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_rotate.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_seal.go create mode 100644 vendor/github.com/hashicorp/vault/api/sys_stepdown.go create mode 100644 vendor/github.com/sethgrid/pester/LICENSE.md create mode 100644 vendor/github.com/sethgrid/pester/README.md create mode 100644 vendor/github.com/sethgrid/pester/main.go diff --git a/vendor/github.com/hashicorp/vault/api/SPEC.md b/vendor/github.com/hashicorp/vault/api/SPEC.md new file mode 100644 index 000000000..15345f390 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/SPEC.md @@ -0,0 +1,611 @@ +FORMAT: 1A + +# vault + +The Vault API gives you full access to the Vault project. + +If you're browsing this API specifiction in GitHub or in raw +format, please excuse some of the odd formatting. This document +is in api-blueprint format that is read by viewers such as +Apiary. + +## Sealed vs. Unsealed + +Whenever an individual Vault server is started, it is started +in the _sealed_ state. In this state, it knows where its data +is located, but the data is encrypted and Vault doesn't have the +encryption keys to access it. Before Vault can operate, it must +be _unsealed_. + +**Note:** Sealing/unsealing has no relationship to _authentication_ +which is separate and still required once the Vault is unsealed. + +Instead of being sealed with a single key, we utilize +[Shamir's Secret Sharing](http://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing) +to shard a key into _n_ parts such that _t_ parts are required +to reconstruct the original key, where `t <= n`. This means that +Vault itself doesn't know the original key, and no single person +has the original key (unless `n = 1`, or `t` parts are given to +a single person). + +Unsealing is done via an unauthenticated +[unseal API](#reference/seal/unseal/unseal). This API takes a single +master shard and progresses the unsealing process. Once all shards +are given, the Vault is either unsealed or resets the unsealing +process if the key was invalid. + +The entire seal/unseal state is server-wide. This allows multiple +distinct operators to use the unseal API (or more likely the +`vault unseal` command) from separate computers/networks and never +have to transmit their key in order to unseal the vault in a +distributed fashion. + +## Transport + +The API is expected to be accessed over a TLS connection at +all times, with a valid certificate that is verified by a well +behaved client. + +## Authentication + +Once the Vault is unsealed, every other operation requires +authentication. There are multiple methods for authentication +that can be enabled (see +[authentication](#reference/authentication)). + +Authentication is done with the login endpoint. The login endpoint +returns an access token that is set as the `X-Vault-Token` header. + +## Help + +To retrieve the help for any API within Vault, including mounted +backends, credential providers, etc. then append `?help=1` to any +URL. If you have valid permission to access the path, then the help text +will be returned with the following structure: + + { + "help": "help text" + } + +## Error Response + +A common JSON structure is always returned to return errors: + + { + "errors": [ + "message", + "another message" + ] + } + +This structure will be sent down for any non-20x HTTP status. + +## HTTP Status Codes + +The following HTTP status codes are used throughout the API. + +- `200` - Success with data. +- `204` - Success, no data returned. +- `400` - Invalid request, missing or invalid data. +- `403` - Forbidden, your authentication details are either + incorrect or you don't have access to this feature. +- `404` - Invalid path. This can both mean that the path truly + doesn't exist or that you don't have permission to view a + specific path. We use 404 in some cases to avoid state leakage. +- `429` - Rate limit exceeded. Try again after waiting some period + of time. +- `500` - Internal server error. An internal error has occurred, + try again later. If the error persists, report a bug. +- `503` - Vault is down for maintenance or is currently sealed. + Try again later. + +# Group Initialization + +## Initialization [/sys/init] +### Initialization Status [GET] +Returns the status of whether the vault is initialized or not. The +vault doesn't have to be unsealed for this operation. + ++ Response 200 (application/json) + + { + "initialized": true + } + +### Initialize [POST] +Initialize the vault. This is an unauthenticated request to initially +setup a new vault. Although this is unauthenticated, it is still safe: +data cannot be in vault prior to initialization, and any future +authentication will fail if you didn't initialize it yourself. +Additionally, once initialized, a vault cannot be reinitialized. + +This API is the only time Vault will ever be aware of your keys, and +the only time the keys will ever be returned in one unit. Care should +be taken to ensure that the output of this request is never logged, +and that the keys are properly distributed. + +The response also contains the initial root token that can be used +as authentication in order to initially configure Vault once it is +unsealed. Just as with the unseal keys, this is the only time Vault is +ever aware of this token. + ++ Request (application/json) + + { + "secret_shares": 5, + "secret_threshold": 3, + } + ++ Response 200 (application/json) + + { + "keys": ["one", "two", "three"], + "root_token": "foo" + } + +# Group Seal/Unseal + +## Seal Status [/sys/seal-status] +### Seal Status [GET] +Returns the status of whether the vault is currently +sealed or not, as well as the progress of unsealing. + +The response has the following attributes: + +- sealed (boolean) - If true, the vault is sealed. Otherwise, + it is unsealed. +- t (int) - The "t" value for the master key, or the number + of shards needed total to unseal the vault. +- n (int) - The "n" value for the master key, or the total + number of shards of the key distributed. +- progress (int) - The number of master key shards that have + been entered so far towards unsealing the vault. + ++ Response 200 (application/json) + + { + "sealed": true, + "t": 3, + "n": 5, + "progress": 1 + } + +## Seal [/sys/seal] +### Seal [PUT] +Seal the vault. + +Sealing the vault locks Vault from any future operations on any +secrets or system configuration until the vault is once again +unsealed. Internally, sealing throws away the keys to access the +encrypted vault data, so Vault is unable to access the data without +unsealing to get the encryption keys. + ++ Response 204 + +## Unseal [/sys/unseal] +### Unseal [PUT] +Unseal the vault. + +Unseal the vault by entering a portion of the master key. The +response object will tell you if the unseal is complete or +only partial. + +If the vault is already unsealed, this does nothing. It is +not an error, the return value just says the vault is unsealed. +Due to the architecture of Vault, we cannot validate whether +any portion of the unseal key given is valid until all keys +are inputted, therefore unsealing an already unsealed vault +is still a success even if the input key is invalid. + ++ Request (application/json) + + { + "key": "value" + } + ++ Response 200 (application/json) + + { + "sealed": true, + "t": 3, + "n": 5, + "progress": 1 + } + +# Group Authentication + +## List Auth Methods [/sys/auth] +### List all auth methods [GET] +Lists all available authentication methods. + +This returns the name of the authentication method as well as +a human-friendly long-form help text for the method that can be +shown to the user as documentation. + ++ Response 200 (application/json) + + { + "token": { + "type": "token", + "description": "Token authentication" + }, + "oauth": { + "type": "oauth", + "description": "OAuth authentication" + } + } + +## Single Auth Method [/sys/auth/{id}] + ++ Parameters + + id (required, string) ... The ID of the auth method. + +### Enable an auth method [PUT] +Enables an authentication method. + +The body of the request depends on the authentication method +being used. Please reference the documentation for the specific +authentication method you're enabling in order to determine what +parameters you must give it. + +If an authentication method is already enabled, then this can be +used to change the configuration, including even the type of +the configuration. + ++ Request (application/json) + + { + "type": "type", + "key": "value", + "key2": "value2" + } + ++ Response 204 + +### Disable an auth method [DELETE] +Disables an authentication method. Previously authenticated sessions +are immediately invalidated. + ++ Response 204 + +# Group Policies + +Policies are named permission sets that identities returned by +credential stores are bound to. This separates _authentication_ +from _authorization_. + +## Policies [/sys/policy] +### List all Policies [GET] + +List all the policies. + ++ Response 200 (application/json) + + { + "policies": ["root"] + } + +## Single Policy [/sys/policy/{id}] + ++ Parameters + + id (required, string) ... The name of the policy + +### Upsert [PUT] + +Create or update a policy with the given ID. + ++ Request (application/json) + + { + "rules": "HCL" + } + ++ Response 204 + +### Delete [DELETE] + +Delete a policy with the given ID. Any identities bound to this +policy will immediately become "deny all" despite already being +authenticated. + ++ Response 204 + +# Group Mounts + +Logical backends are mounted at _mount points_, similar to +filesystems. This allows you to mount the "aws" logical backend +at the "aws-us-east" path, so all access is at `/aws-us-east/keys/foo` +for example. This enables multiple logical backends to be enabled. + +## Mounts [/sys/mounts] +### List all mounts [GET] + +Lists all the active mount points. + ++ Response 200 (application/json) + + { + "aws": { + "type": "aws", + "description": "AWS" + }, + "pg": { + "type": "postgresql", + "description": "PostgreSQL dynamic users" + } + } + +## Single Mount [/sys/mounts/{path}] +### New Mount [POST] + +Mount a logical backend to a new path. + +Configuration for this new backend is done via the normal +read/write mechanism once it is mounted. + ++ Request (application/json) + + { + "type": "aws", + "description": "EU AWS tokens" + } + ++ Response 204 + +### Unmount [DELETE] + +Unmount a mount point. + ++ Response 204 + +## Remount [/sys/remount] +### Remount [POST] + +Move an already-mounted backend to a new path. + ++ Request (application/json) + + { + "from": "aws", + "to": "aws-east" + } + ++ Response 204 + +# Group Audit Backends + +Audit backends are responsible for shuttling the audit logs that +Vault generates to a durable system for future querying. By default, +audit logs are not stored anywhere. + +## Audit Backends [/sys/audit] +### List Enabled Audit Backends [GET] + +List all the enabled audit backends + ++ Response 200 (application/json) + + { + "file": { + "type": "file", + "description": "Send audit logs to a file", + "options": {} + } + } + +## Single Audit Backend [/sys/audit/{path}] + ++ Parameters + + path (required, string) ... The path where the audit backend is mounted + +### Enable [PUT] + +Enable an audit backend. + ++ Request (application/json) + + { + "type": "file", + "description": "send to a file", + "options": { + "path": "/var/log/vault.audit.log" + } + } + ++ Response 204 + +### Disable [DELETE] + +Disable an audit backend. + ++ Request (application/json) + ++ Response 204 + +# Group Secrets + +## Generic [/{mount}/{path}] + +This group documents the general format of reading and writing +to Vault. The exact structure of the keyspace is defined by the +logical backends in use, so documentation related to +a specific backend should be referenced for details on what keys +and routes are expected. + +The path for examples are `/prefix/path`, but in practice +these will be defined by the backends that are mounted. For +example, reading an AWS key might be at the `/aws/root` path. +These paths are defined by the logical backends. + ++ Parameters + + mount (required, string) ... The mount point for the + logical backend. Example: `aws`. + + path (optional, string) ... The path within the backend + to read or write data. + +### Read [GET] + +Read data from vault. + +The data read from the vault can either be a secret or +arbitrary configuration data. The type of data returned +depends on the path, and is defined by the logical backend. + +If the return value is a secret, then the return structure +is a mixture of arbitrary key/value along with the following +fields which are guaranteed to exist: + +- `lease_id` (string) - A unique ID used for renewal and + revocation. + +- `renewable` (bool) - If true, then this key can be renewed. + If a key can't be renewed, then a new key must be requested + after the lease duration period. + +- `lease_duration` (int) - The time in seconds that a secret is + valid for before it must be renewed. + +- `lease_duration_max` (int) - The maximum amount of time in + seconds that a secret is valid for. This will always be + greater than or equal to `lease_duration`. The difference + between this and `lease_duration` is an overlap window + where multiple keys may be valid. + +If the return value is not a secret, then the return structure +is an arbitrary JSON object. + ++ Response 200 (application/json) + + { + "lease_id": "UUID", + "lease_duration": 3600, + "key": "value" + } + +### Write [PUT] + +Write data to vault. + +The behavior and arguments to the write are defined by +the logical backend. + ++ Request (application/json) + + { + "key": "value" + } + ++ Response 204 + +# Group Lease Management + +## Renew Key [/sys/renew/{id}] + ++ Parameters + + id (required, string) ... The `lease_id` of the secret + to renew. + +### Renew [PUT] + ++ Response 200 (application/json) + + { + "lease_id": "...", + "lease_duration": 3600, + "access_key": "foo", + "secret_key": "bar" + } + +## Revoke Key [/sys/revoke/{id}] + ++ Parameters + + id (required, string) ... The `lease_id` of the secret + to revoke. + +### Revoke [PUT] + ++ Response 204 + +# Group Backend: AWS + +## Root Key [/aws/root] +### Set the Key [PUT] + +Set the root key that the logical backend will use to create +new secrets, IAM policies, etc. + ++ Request (application/json) + + { + "access_key": "key", + "secret_key": "key", + "region": "us-east-1" + } + ++ Response 204 + +## Policies [/aws/policies] +### List Policies [GET] + +List all the policies that can be used to create keys. + ++ Response 200 (application/json) + + [{ + "name": "root", + "description": "Root access" + }, { + "name": "web-deploy", + "description": "Enough permissions to deploy the web app." + }] + +## Single Policy [/aws/policies/{name}] + ++ Parameters + + name (required, string) ... Name of the policy. + +### Read [GET] + +Read a policy. + ++ Response 200 (application/json) + + { + "policy": "base64-encoded policy" + } + +### Upsert [PUT] + +Create or update a policy. + ++ Request (application/json) + + { + "policy": "base64-encoded policy" + } + ++ Response 204 + +### Delete [DELETE] + +Delete the policy with the given name. + ++ Response 204 + +## Generate Access Keys [/aws/keys/{policy}] +### Create [GET] + +This generates a new keypair for the given policy. + ++ Parameters + + policy (required, string) ... The policy under which to create + the key pair. + ++ Response 200 (application/json) + + { + "lease_id": "...", + "lease_duration": 3600, + "access_key": "foo", + "secret_key": "bar" + } diff --git a/vendor/github.com/hashicorp/vault/api/auth.go b/vendor/github.com/hashicorp/vault/api/auth.go new file mode 100644 index 000000000..da870c111 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/auth.go @@ -0,0 +1,11 @@ +package api + +// Auth is used to perform credential backend related operations. +type Auth struct { + c *Client +} + +// Auth is used to return the client for credential-backend API calls. +func (c *Client) Auth() *Auth { + return &Auth{c: c} +} diff --git a/vendor/github.com/hashicorp/vault/api/auth_token.go b/vendor/github.com/hashicorp/vault/api/auth_token.go new file mode 100644 index 000000000..aff10f410 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/auth_token.go @@ -0,0 +1,223 @@ +package api + +// TokenAuth is used to perform token backend operations on Vault +type TokenAuth struct { + c *Client +} + +// Token is used to return the client for token-backend API calls +func (a *Auth) Token() *TokenAuth { + return &TokenAuth{c: a.c} +} + +func (c *TokenAuth) Create(opts *TokenCreateRequest) (*Secret, error) { + r := c.c.NewRequest("POST", "/v1/auth/token/create") + if err := r.SetJSONBody(opts); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *TokenAuth) CreateOrphan(opts *TokenCreateRequest) (*Secret, error) { + r := c.c.NewRequest("POST", "/v1/auth/token/create-orphan") + if err := r.SetJSONBody(opts); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *TokenAuth) CreateWithRole(opts *TokenCreateRequest, roleName string) (*Secret, error) { + r := c.c.NewRequest("POST", "/v1/auth/token/create/"+roleName) + if err := r.SetJSONBody(opts); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *TokenAuth) Lookup(token string) (*Secret, error) { + r := c.c.NewRequest("POST", "/v1/auth/token/lookup") + if err := r.SetJSONBody(map[string]interface{}{ + "token": token, + }); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *TokenAuth) LookupAccessor(accessor string) (*Secret, error) { + r := c.c.NewRequest("POST", "/v1/auth/token/lookup-accessor") + if err := r.SetJSONBody(map[string]interface{}{ + "accessor": accessor, + }); err != nil { + return nil, err + } + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *TokenAuth) LookupSelf() (*Secret, error) { + r := c.c.NewRequest("GET", "/v1/auth/token/lookup-self") + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *TokenAuth) Renew(token string, increment int) (*Secret, error) { + r := c.c.NewRequest("PUT", "/v1/auth/token/renew") + if err := r.SetJSONBody(map[string]interface{}{ + "token": token, + "increment": increment, + }); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *TokenAuth) RenewSelf(increment int) (*Secret, error) { + r := c.c.NewRequest("PUT", "/v1/auth/token/renew-self") + + body := map[string]interface{}{"increment": increment} + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +// RevokeAccessor revokes a token associated with the given accessor +// along with all the child tokens. +func (c *TokenAuth) RevokeAccessor(accessor string) error { + r := c.c.NewRequest("POST", "/v1/auth/token/revoke-accessor") + if err := r.SetJSONBody(map[string]interface{}{ + "accessor": accessor, + }); err != nil { + return err + } + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// RevokeOrphan revokes a token without revoking the tree underneath it (so +// child tokens are orphaned rather than revoked) +func (c *TokenAuth) RevokeOrphan(token string) error { + r := c.c.NewRequest("PUT", "/v1/auth/token/revoke-orphan") + if err := r.SetJSONBody(map[string]interface{}{ + "token": token, + }); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// RevokeSelf revokes the token making the call. The `token` parameter is kept +// for backwards compatibility but is ignored; only the client's set token has +// an effect. +func (c *TokenAuth) RevokeSelf(token string) error { + r := c.c.NewRequest("PUT", "/v1/auth/token/revoke-self") + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// RevokeTree is the "normal" revoke operation that revokes the given token and +// the entire tree underneath -- all of its child tokens, their child tokens, +// etc. +func (c *TokenAuth) RevokeTree(token string) error { + r := c.c.NewRequest("PUT", "/v1/auth/token/revoke") + if err := r.SetJSONBody(map[string]interface{}{ + "token": token, + }); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// TokenCreateRequest is the options structure for creating a token. +type TokenCreateRequest struct { + ID string `json:"id,omitempty"` + Policies []string `json:"policies,omitempty"` + Metadata map[string]string `json:"meta,omitempty"` + Lease string `json:"lease,omitempty"` + TTL string `json:"ttl,omitempty"` + ExplicitMaxTTL string `json:"explicit_max_ttl,omitempty"` + Period string `json:"period,omitempty"` + NoParent bool `json:"no_parent,omitempty"` + NoDefaultPolicy bool `json:"no_default_policy,omitempty"` + DisplayName string `json:"display_name"` + NumUses int `json:"num_uses"` + Renewable *bool `json:"renewable,omitempty"` +} diff --git a/vendor/github.com/hashicorp/vault/api/client.go b/vendor/github.com/hashicorp/vault/api/client.go new file mode 100644 index 000000000..4aee40c0a --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/client.go @@ -0,0 +1,416 @@ +package api + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-rootcerts" + "github.com/sethgrid/pester" +) + +const EnvVaultAddress = "VAULT_ADDR" +const EnvVaultCACert = "VAULT_CACERT" +const EnvVaultCAPath = "VAULT_CAPATH" +const EnvVaultClientCert = "VAULT_CLIENT_CERT" +const EnvVaultClientKey = "VAULT_CLIENT_KEY" +const EnvVaultInsecure = "VAULT_SKIP_VERIFY" +const EnvVaultTLSServerName = "VAULT_TLS_SERVER_NAME" +const EnvVaultWrapTTL = "VAULT_WRAP_TTL" +const EnvVaultMaxRetries = "VAULT_MAX_RETRIES" + +// WrappingLookupFunc is a function that, given an HTTP verb and a path, +// returns an optional string duration to be used for response wrapping (e.g. +// "15s", or simply "15"). The path will not begin with "/v1/" or "v1/" or "/", +// however, end-of-path forward slashes are not trimmed, so must match your +// called path precisely. +type WrappingLookupFunc func(operation, path string) string + +// Config is used to configure the creation of the client. +type Config struct { + // Address is the address of the Vault server. This should be a complete + // URL such as "http://vault.example.com". If you need a custom SSL + // cert or want to enable insecure mode, you need to specify a custom + // HttpClient. + Address string + + // HttpClient is the HTTP client to use, which will currently always have the + // same values as http.DefaultClient. This is used to control redirect behavior. + HttpClient *http.Client + + redirectSetup sync.Once + + // MaxRetries controls the maximum number of times to retry when a 5xx error + // occurs. Set to 0 or less to disable retrying. + MaxRetries int +} + +// TLSConfig contains the parameters needed to configure TLS on the HTTP client +// used to communicate with Vault. +type TLSConfig struct { + // CACert is the path to a PEM-encoded CA cert file to use to verify the + // Vault server SSL certificate. + CACert string + + // CAPath is the path to a directory of PEM-encoded CA cert files to verify + // the Vault server SSL certificate. + CAPath string + + // ClientCert is the path to the certificate for Vault communication + ClientCert string + + // ClientKey is the path to the private key for Vault communication + ClientKey string + + // TLSServerName, if set, is used to set the SNI host when connecting via + // TLS. + TLSServerName string + + // Insecure enables or disables SSL verification + Insecure bool +} + +// DefaultConfig returns a default configuration for the client. It is +// safe to modify the return value of this function. +// +// The default Address is https://127.0.0.1:8200, but this can be overridden by +// setting the `VAULT_ADDR` environment variable. +func DefaultConfig() *Config { + config := &Config{ + Address: "https://127.0.0.1:8200", + + HttpClient: cleanhttp.DefaultClient(), + } + config.HttpClient.Timeout = time.Second * 60 + transport := config.HttpClient.Transport.(*http.Transport) + transport.TLSHandshakeTimeout = 10 * time.Second + transport.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + + if v := os.Getenv(EnvVaultAddress); v != "" { + config.Address = v + } + + config.MaxRetries = pester.DefaultClient.MaxRetries + + return config +} + +// ConfigureTLS takes a set of TLS configurations and applies those to the the HTTP client. +func (c *Config) ConfigureTLS(t *TLSConfig) error { + + if c.HttpClient == nil { + return fmt.Errorf("config HTTP Client must be set") + } + + var clientCert tls.Certificate + foundClientCert := false + if t.CACert != "" || t.CAPath != "" || t.ClientCert != "" || t.ClientKey != "" || t.Insecure { + if t.ClientCert != "" && t.ClientKey != "" { + var err error + clientCert, err = tls.LoadX509KeyPair(t.ClientCert, t.ClientKey) + if err != nil { + return err + } + foundClientCert = true + } else if t.ClientCert != "" || t.ClientKey != "" { + return fmt.Errorf("Both client cert and client key must be provided") + } + } + + clientTLSConfig := c.HttpClient.Transport.(*http.Transport).TLSClientConfig + rootConfig := &rootcerts.Config{ + CAFile: t.CACert, + CAPath: t.CAPath, + } + if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil { + return err + } + + clientTLSConfig.InsecureSkipVerify = t.Insecure + + if foundClientCert { + clientTLSConfig.Certificates = []tls.Certificate{clientCert} + } + if t.TLSServerName != "" { + clientTLSConfig.ServerName = t.TLSServerName + } + + return nil +} + +// ReadEnvironment reads configuration information from the +// environment. If there is an error, no configuration value +// is updated. +func (c *Config) ReadEnvironment() error { + var envAddress string + var envCACert string + var envCAPath string + var envClientCert string + var envClientKey string + var envInsecure bool + var envTLSServerName string + var envMaxRetries *uint64 + + // Parse the environment variables + if v := os.Getenv(EnvVaultAddress); v != "" { + envAddress = v + } + if v := os.Getenv(EnvVaultMaxRetries); v != "" { + maxRetries, err := strconv.ParseUint(v, 10, 32) + if err != nil { + return err + } + envMaxRetries = &maxRetries + } + if v := os.Getenv(EnvVaultCACert); v != "" { + envCACert = v + } + if v := os.Getenv(EnvVaultCAPath); v != "" { + envCAPath = v + } + if v := os.Getenv(EnvVaultClientCert); v != "" { + envClientCert = v + } + if v := os.Getenv(EnvVaultClientKey); v != "" { + envClientKey = v + } + if v := os.Getenv(EnvVaultInsecure); v != "" { + var err error + envInsecure, err = strconv.ParseBool(v) + if err != nil { + return fmt.Errorf("Could not parse VAULT_SKIP_VERIFY") + } + } + if v := os.Getenv(EnvVaultTLSServerName); v != "" { + envTLSServerName = v + } + + // Configure the HTTP clients TLS configuration. + t := &TLSConfig{ + CACert: envCACert, + CAPath: envCAPath, + ClientCert: envClientCert, + ClientKey: envClientKey, + TLSServerName: envTLSServerName, + Insecure: envInsecure, + } + if err := c.ConfigureTLS(t); err != nil { + return err + } + + if envAddress != "" { + c.Address = envAddress + } + + if envMaxRetries != nil { + c.MaxRetries = int(*envMaxRetries) + 1 + } + + return nil +} + +// Client is the client to the Vault API. Create a client with +// NewClient. +type Client struct { + addr *url.URL + config *Config + token string + wrappingLookupFunc WrappingLookupFunc +} + +// NewClient returns a new client for the given configuration. +// +// If the environment variable `VAULT_TOKEN` is present, the token will be +// automatically added to the client. Otherwise, you must manually call +// `SetToken()`. +func NewClient(c *Config) (*Client, error) { + if c == nil { + c = DefaultConfig() + if err := c.ReadEnvironment(); err != nil { + return nil, fmt.Errorf("error reading environment: %v", err) + } + } + + u, err := url.Parse(c.Address) + if err != nil { + return nil, err + } + + if c.HttpClient == nil { + c.HttpClient = DefaultConfig().HttpClient + } + + redirFunc := func() { + // Ensure redirects are not automatically followed + // Note that this is sane for the API client as it has its own + // redirect handling logic (and thus also for command/meta), + // but in e.g. http_test actual redirect handling is necessary + c.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + // Returning this value causes the Go net library to not close the + // response body and nil out the error. Otherwise pester tries + // three times on every redirect because it sees an error from this + // function being passed through. + return http.ErrUseLastResponse + } + } + + c.redirectSetup.Do(redirFunc) + + client := &Client{ + addr: u, + config: c, + } + + if token := os.Getenv("VAULT_TOKEN"); token != "" { + client.SetToken(token) + } + + return client, nil +} + +// Sets the address of Vault in the client. The format of address should be +// "://:". Setting this on a client will override the +// value of VAULT_ADDR environment variable. +func (c *Client) SetAddress(addr string) error { + var err error + if c.addr, err = url.Parse(addr); err != nil { + return fmt.Errorf("failed to set address: %v", err) + } + + return nil +} + +// SetWrappingLookupFunc sets a lookup function that returns desired wrap TTLs +// for a given operation and path +func (c *Client) SetWrappingLookupFunc(lookupFunc WrappingLookupFunc) { + c.wrappingLookupFunc = lookupFunc +} + +// Token returns the access token being used by this client. It will +// return the empty string if there is no token set. +func (c *Client) Token() string { + return c.token +} + +// SetToken sets the token directly. This won't perform any auth +// verification, it simply sets the token properly for future requests. +func (c *Client) SetToken(v string) { + c.token = v +} + +// ClearToken deletes the token if it is set or does nothing otherwise. +func (c *Client) ClearToken() { + c.token = "" +} + +// NewRequest creates a new raw request object to query the Vault server +// configured for this client. This is an advanced method and generally +// doesn't need to be called externally. +func (c *Client) NewRequest(method, path string) *Request { + req := &Request{ + Method: method, + URL: &url.URL{ + Scheme: c.addr.Scheme, + Host: c.addr.Host, + Path: path, + }, + ClientToken: c.token, + Params: make(map[string][]string), + } + + var lookupPath string + switch { + case strings.HasPrefix(path, "/v1/"): + lookupPath = strings.TrimPrefix(path, "/v1/") + case strings.HasPrefix(path, "v1/"): + lookupPath = strings.TrimPrefix(path, "v1/") + default: + lookupPath = path + } + if c.wrappingLookupFunc != nil { + req.WrapTTL = c.wrappingLookupFunc(method, lookupPath) + } else { + req.WrapTTL = DefaultWrappingLookupFunc(method, lookupPath) + } + + return req +} + +// RawRequest performs the raw request given. This request may be against +// a Vault server not configured with this client. This is an advanced operation +// that generally won't need to be called externally. +func (c *Client) RawRequest(r *Request) (*Response, error) { + redirectCount := 0 +START: + req, err := r.ToHTTP() + if err != nil { + return nil, err + } + + client := pester.NewExtendedClient(c.config.HttpClient) + client.Backoff = pester.LinearJitterBackoff + client.MaxRetries = c.config.MaxRetries + + var result *Response + resp, err := client.Do(req) + if resp != nil { + result = &Response{Response: resp} + } + if err != nil { + if strings.Contains(err.Error(), "tls: oversized") { + err = fmt.Errorf( + "%s\n\n"+ + "This error usually means that the server is running with TLS disabled\n"+ + "but the client is configured to use TLS. Please either enable TLS\n"+ + "on the server or run the client with -address set to an address\n"+ + "that uses the http protocol:\n\n"+ + " vault -address http://
\n\n"+ + "You can also set the VAULT_ADDR environment variable:\n\n\n"+ + " VAULT_ADDR=http://
vault \n\n"+ + "where
is replaced by the actual address to the server.", + err) + } + return result, err + } + + // Check for a redirect, only allowing for a single redirect + if (resp.StatusCode == 301 || resp.StatusCode == 302 || resp.StatusCode == 307) && redirectCount == 0 { + // Parse the updated location + respLoc, err := resp.Location() + if err != nil { + return result, err + } + + // Ensure a protocol downgrade doesn't happen + if req.URL.Scheme == "https" && respLoc.Scheme != "https" { + return result, fmt.Errorf("redirect would cause protocol downgrade") + } + + // Update the request + r.URL = respLoc + + // Reset the request body if any + if err := r.ResetJSONBody(); err != nil { + return result, err + } + + // Retry the request + redirectCount++ + goto START + } + + if err := result.Error(); err != nil { + return result, err + } + + return result, nil +} diff --git a/vendor/github.com/hashicorp/vault/api/help.go b/vendor/github.com/hashicorp/vault/api/help.go new file mode 100644 index 000000000..b9ae100bc --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/help.go @@ -0,0 +1,25 @@ +package api + +import ( + "fmt" +) + +// Help reads the help information for the given path. +func (c *Client) Help(path string) (*Help, error) { + r := c.NewRequest("GET", fmt.Sprintf("/v1/%s", path)) + r.Params.Add("help", "1") + resp, err := c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result Help + err = resp.DecodeJSON(&result) + return &result, err +} + +type Help struct { + Help string `json:"help"` + SeeAlso []string `json:"see_also"` +} diff --git a/vendor/github.com/hashicorp/vault/api/logical.go b/vendor/github.com/hashicorp/vault/api/logical.go new file mode 100644 index 000000000..9753e9668 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/logical.go @@ -0,0 +1,176 @@ +package api + +import ( + "bytes" + "fmt" + "net/http" + "os" + + "github.com/hashicorp/vault/helper/jsonutil" +) + +const ( + wrappedResponseLocation = "cubbyhole/response" +) + +var ( + // The default TTL that will be used with `sys/wrapping/wrap`, can be + // changed + DefaultWrappingTTL = "5m" + + // The default function used if no other function is set, which honors the + // env var and wraps `sys/wrapping/wrap` + DefaultWrappingLookupFunc = func(operation, path string) string { + if os.Getenv(EnvVaultWrapTTL) != "" { + return os.Getenv(EnvVaultWrapTTL) + } + + if (operation == "PUT" || operation == "POST") && path == "sys/wrapping/wrap" { + return DefaultWrappingTTL + } + + return "" + } +) + +// Logical is used to perform logical backend operations on Vault. +type Logical struct { + c *Client +} + +// Logical is used to return the client for logical-backend API calls. +func (c *Client) Logical() *Logical { + return &Logical{c: c} +} + +func (c *Logical) Read(path string) (*Secret, error) { + r := c.c.NewRequest("GET", "/v1/"+path) + resp, err := c.c.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == 404 { + return nil, nil + } + if err != nil { + return nil, err + } + + return ParseSecret(resp.Body) +} + +func (c *Logical) List(path string) (*Secret, error) { + r := c.c.NewRequest("LIST", "/v1/"+path) + // Set this for broader compatibility, but we use LIST above to be able to + // handle the wrapping lookup function + r.Method = "GET" + r.Params.Set("list", "true") + resp, err := c.c.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == 404 { + return nil, nil + } + if err != nil { + return nil, err + } + + return ParseSecret(resp.Body) +} + +func (c *Logical) Write(path string, data map[string]interface{}) (*Secret, error) { + r := c.c.NewRequest("PUT", "/v1/"+path) + if err := r.SetJSONBody(data); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + if resp.StatusCode == 200 { + return ParseSecret(resp.Body) + } + + return nil, nil +} + +func (c *Logical) Delete(path string) (*Secret, error) { + r := c.c.NewRequest("DELETE", "/v1/"+path) + resp, err := c.c.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return nil, err + } + + if resp.StatusCode == 200 { + return ParseSecret(resp.Body) + } + + return nil, nil +} + +func (c *Logical) Unwrap(wrappingToken string) (*Secret, error) { + var data map[string]interface{} + if wrappingToken != "" && wrappingToken != c.c.Token() { + data = map[string]interface{}{ + "token": wrappingToken, + } + } + + r := c.c.NewRequest("PUT", "/v1/sys/wrapping/unwrap") + if err := r.SetJSONBody(data); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + if err != nil && resp.StatusCode != 404 { + return nil, err + } + + switch resp.StatusCode { + case http.StatusOK: // New method is supported + return ParseSecret(resp.Body) + case http.StatusNotFound: // Fall back to old method + default: + return nil, nil + } + + if wrappingToken != "" { + origToken := c.c.Token() + defer c.c.SetToken(origToken) + c.c.SetToken(wrappingToken) + } + + secret, err := c.Read(wrappedResponseLocation) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", wrappedResponseLocation, err) + } + if secret == nil { + return nil, fmt.Errorf("no value found at %s", wrappedResponseLocation) + } + if secret.Data == nil { + return nil, fmt.Errorf("\"data\" not found in wrapping response") + } + if _, ok := secret.Data["response"]; !ok { + return nil, fmt.Errorf("\"response\" not found in wrapping response \"data\" map") + } + + wrappedSecret := new(Secret) + buf := bytes.NewBufferString(secret.Data["response"].(string)) + if err := jsonutil.DecodeJSONFromReader(buf, wrappedSecret); err != nil { + return nil, fmt.Errorf("error unmarshaling wrapped secret: %s", err) + } + + return wrappedSecret, nil +} diff --git a/vendor/github.com/hashicorp/vault/api/request.go b/vendor/github.com/hashicorp/vault/api/request.go new file mode 100644 index 000000000..8f22dd572 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/request.go @@ -0,0 +1,71 @@ +package api + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" +) + +// Request is a raw request configuration structure used to initiate +// API requests to the Vault server. +type Request struct { + Method string + URL *url.URL + Params url.Values + ClientToken string + WrapTTL string + Obj interface{} + Body io.Reader + BodySize int64 +} + +// SetJSONBody is used to set a request body that is a JSON-encoded value. +func (r *Request) SetJSONBody(val interface{}) error { + buf := bytes.NewBuffer(nil) + enc := json.NewEncoder(buf) + if err := enc.Encode(val); err != nil { + return err + } + + r.Obj = val + r.Body = buf + r.BodySize = int64(buf.Len()) + return nil +} + +// ResetJSONBody is used to reset the body for a redirect +func (r *Request) ResetJSONBody() error { + if r.Body == nil { + return nil + } + return r.SetJSONBody(r.Obj) +} + +// ToHTTP turns this request into a valid *http.Request for use with the +// net/http package. +func (r *Request) ToHTTP() (*http.Request, error) { + // Encode the query parameters + r.URL.RawQuery = r.Params.Encode() + + // Create the HTTP request + req, err := http.NewRequest(r.Method, r.URL.RequestURI(), r.Body) + if err != nil { + return nil, err + } + + req.URL.Scheme = r.URL.Scheme + req.URL.Host = r.URL.Host + req.Host = r.URL.Host + + if len(r.ClientToken) != 0 { + req.Header.Set("X-Vault-Token", r.ClientToken) + } + + if len(r.WrapTTL) != 0 { + req.Header.Set("X-Vault-Wrap-TTL", r.WrapTTL) + } + + return req, nil +} diff --git a/vendor/github.com/hashicorp/vault/api/response.go b/vendor/github.com/hashicorp/vault/api/response.go new file mode 100644 index 000000000..7c8ac9f97 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/response.go @@ -0,0 +1,72 @@ +package api + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "github.com/hashicorp/vault/helper/jsonutil" +) + +// Response is a raw response that wraps an HTTP response. +type Response struct { + *http.Response +} + +// DecodeJSON will decode the response body to a JSON structure. This +// will consume the response body, but will not close it. Close must +// still be called. +func (r *Response) DecodeJSON(out interface{}) error { + return jsonutil.DecodeJSONFromReader(r.Body, out) +} + +// Error returns an error response if there is one. If there is an error, +// this will fully consume the response body, but will not close it. The +// body must still be closed manually. +func (r *Response) Error() error { + // 200 to 399 are okay status codes + if r.StatusCode >= 200 && r.StatusCode < 400 { + return nil + } + + // We have an error. Let's copy the body into our own buffer first, + // so that if we can't decode JSON, we can at least copy it raw. + var bodyBuf bytes.Buffer + if _, err := io.Copy(&bodyBuf, r.Body); err != nil { + return err + } + + // Decode the error response if we can. Note that we wrap the bodyBuf + // in a bytes.Reader here so that the JSON decoder doesn't move the + // read pointer for the original buffer. + var resp ErrorResponse + if err := jsonutil.DecodeJSON(bodyBuf.Bytes(), &resp); err != nil { + // Ignore the decoding error and just drop the raw response + return fmt.Errorf( + "Error making API request.\n\n"+ + "URL: %s %s\n"+ + "Code: %d. Raw Message:\n\n%s", + r.Request.Method, r.Request.URL.String(), + r.StatusCode, bodyBuf.String()) + } + + var errBody bytes.Buffer + errBody.WriteString(fmt.Sprintf( + "Error making API request.\n\n"+ + "URL: %s %s\n"+ + "Code: %d. Errors:\n\n", + r.Request.Method, r.Request.URL.String(), + r.StatusCode)) + for _, err := range resp.Errors { + errBody.WriteString(fmt.Sprintf("* %s", err)) + } + + return fmt.Errorf(errBody.String()) +} + +// ErrorResponse is the raw structure of errors when they're returned by the +// HTTP API. +type ErrorResponse struct { + Errors []string +} diff --git a/vendor/github.com/hashicorp/vault/api/secret.go b/vendor/github.com/hashicorp/vault/api/secret.go new file mode 100644 index 000000000..14924f9d0 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/secret.go @@ -0,0 +1,68 @@ +package api + +import ( + "io" + "time" + + "github.com/hashicorp/vault/helper/jsonutil" +) + +// Secret is the structure returned for every secret within Vault. +type Secret struct { + // The request ID that generated this response + RequestID string `json:"request_id"` + + LeaseID string `json:"lease_id"` + LeaseDuration int `json:"lease_duration"` + Renewable bool `json:"renewable"` + + // Data is the actual contents of the secret. The format of the data + // is arbitrary and up to the secret backend. + Data map[string]interface{} `json:"data"` + + // Warnings contains any warnings related to the operation. These + // are not issues that caused the command to fail, but that the + // client should be aware of. + Warnings []string `json:"warnings"` + + // Auth, if non-nil, means that there was authentication information + // attached to this response. + Auth *SecretAuth `json:"auth,omitempty"` + + // WrapInfo, if non-nil, means that the initial response was wrapped in the + // cubbyhole of the given token (which has a TTL of the given number of + // seconds) + WrapInfo *SecretWrapInfo `json:"wrap_info,omitempty"` +} + +// SecretWrapInfo contains wrapping information if we have it. If what is +// contained is an authentication token, the accessor for the token will be +// available in WrappedAccessor. +type SecretWrapInfo struct { + Token string `json:"token"` + TTL int `json:"ttl"` + CreationTime time.Time `json:"creation_time"` + WrappedAccessor string `json:"wrapped_accessor"` +} + +// SecretAuth is the structure containing auth information if we have it. +type SecretAuth struct { + ClientToken string `json:"client_token"` + Accessor string `json:"accessor"` + Policies []string `json:"policies"` + Metadata map[string]string `json:"metadata"` + + LeaseDuration int `json:"lease_duration"` + Renewable bool `json:"renewable"` +} + +// ParseSecret is used to parse a secret value from JSON from an io.Reader. +func ParseSecret(r io.Reader) (*Secret, error) { + // First decode the JSON into a map[string]interface{} + var secret Secret + if err := jsonutil.DecodeJSONFromReader(r, &secret); err != nil { + return nil, err + } + + return &secret, nil +} diff --git a/vendor/github.com/hashicorp/vault/api/ssh.go b/vendor/github.com/hashicorp/vault/api/ssh.go new file mode 100644 index 000000000..7c3e56bb4 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/ssh.go @@ -0,0 +1,38 @@ +package api + +import "fmt" + +// SSH is used to return a client to invoke operations on SSH backend. +type SSH struct { + c *Client + MountPoint string +} + +// SSH returns the client for logical-backend API calls. +func (c *Client) SSH() *SSH { + return c.SSHWithMountPoint(SSHHelperDefaultMountPoint) +} + +// SSHWithMountPoint returns the client with specific SSH mount point. +func (c *Client) SSHWithMountPoint(mountPoint string) *SSH { + return &SSH{ + c: c, + MountPoint: mountPoint, + } +} + +// Credential invokes the SSH backend API to create a credential to establish an SSH session. +func (c *SSH) Credential(role string, data map[string]interface{}) (*Secret, error) { + r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/%s/creds/%s", c.MountPoint, role)) + if err := r.SetJSONBody(data); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} diff --git a/vendor/github.com/hashicorp/vault/api/ssh_agent.go b/vendor/github.com/hashicorp/vault/api/ssh_agent.go new file mode 100644 index 000000000..729fd99c4 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/ssh_agent.go @@ -0,0 +1,257 @@ +package api + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-rootcerts" + "github.com/hashicorp/hcl" + "github.com/hashicorp/hcl/hcl/ast" + "github.com/mitchellh/mapstructure" +) + +const ( + // SSHHelperDefaultMountPoint is the default path at which SSH backend will be + // mounted in the Vault server. + SSHHelperDefaultMountPoint = "ssh" + + // VerifyEchoRequest is the echo request message sent as OTP by the helper. + VerifyEchoRequest = "verify-echo-request" + + // VerifyEchoResponse is the echo response message sent as a response to OTP + // matching echo request. + VerifyEchoResponse = "verify-echo-response" +) + +// SSHHelper is a structure representing a vault-ssh-helper which can talk to vault server +// in order to verify the OTP entered by the user. It contains the path at which +// SSH backend is mounted at the server. +type SSHHelper struct { + c *Client + MountPoint string +} + +// SSHVerifyResponse is a structure representing the fields in Vault server's +// response. +type SSHVerifyResponse struct { + // Usually empty. If the request OTP is echo request message, this will + // be set to the corresponding echo response message. + Message string `json:"message" structs:"message" mapstructure:"message"` + + // Username associated with the OTP + Username string `json:"username" structs:"username" mapstructure:"username"` + + // IP associated with the OTP + IP string `json:"ip" structs:"ip" mapstructure:"ip"` + + // Name of the role against which the OTP was issued + RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"` +} + +// SSHHelperConfig is a structure which represents the entries from the vault-ssh-helper's configuration file. +type SSHHelperConfig struct { + VaultAddr string `hcl:"vault_addr"` + SSHMountPoint string `hcl:"ssh_mount_point"` + CACert string `hcl:"ca_cert"` + CAPath string `hcl:"ca_path"` + AllowedCidrList string `hcl:"allowed_cidr_list"` + AllowedRoles string `hcl:"allowed_roles"` + TLSSkipVerify bool `hcl:"tls_skip_verify"` + TLSServerName string `hcl:"tls_server_name"` +} + +// SetTLSParameters sets the TLS parameters for this SSH agent. +func (c *SSHHelperConfig) SetTLSParameters(clientConfig *Config, certPool *x509.CertPool) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: c.TLSSkipVerify, + MinVersion: tls.VersionTLS12, + RootCAs: certPool, + ServerName: c.TLSServerName, + } + + transport := cleanhttp.DefaultTransport() + transport.TLSClientConfig = tlsConfig + clientConfig.HttpClient.Transport = transport +} + +// Returns true if any of the following conditions are true: +// * CA cert is configured +// * CA path is configured +// * configured to skip certificate verification +// * TLS server name is configured +// +func (c *SSHHelperConfig) shouldSetTLSParameters() bool { + return c.CACert != "" || c.CAPath != "" || c.TLSServerName != "" || c.TLSSkipVerify +} + +// NewClient returns a new client for the configuration. This client will be used by the +// vault-ssh-helper to communicate with Vault server and verify the OTP entered by user. +// If the configuration supplies Vault SSL certificates, then the client will +// have TLS configured in its transport. +func (c *SSHHelperConfig) NewClient() (*Client, error) { + // Creating a default client configuration for communicating with vault server. + clientConfig := DefaultConfig() + + // Pointing the client to the actual address of vault server. + clientConfig.Address = c.VaultAddr + + // Check if certificates are provided via config file. + if c.shouldSetTLSParameters() { + rootConfig := &rootcerts.Config{ + CAFile: c.CACert, + CAPath: c.CAPath, + } + certPool, err := rootcerts.LoadCACerts(rootConfig) + if err != nil { + return nil, err + } + // Enable TLS on the HTTP client information + c.SetTLSParameters(clientConfig, certPool) + } + + // Creating the client object for the given configuration + client, err := NewClient(clientConfig) + if err != nil { + return nil, err + } + + return client, nil +} + +// LoadSSHHelperConfig loads ssh-helper's configuration from the file and populates the corresponding +// in-memory structure. +// +// Vault address is a required parameter. +// Mount point defaults to "ssh". +func LoadSSHHelperConfig(path string) (*SSHHelperConfig, error) { + contents, err := ioutil.ReadFile(path) + if err != nil && !os.IsNotExist(err) { + return nil, multierror.Prefix(err, "ssh_helper:") + } + return ParseSSHHelperConfig(string(contents)) +} + +// ParseSSHHelperConfig parses the given contents as a string for the SSHHelper +// configuration. +func ParseSSHHelperConfig(contents string) (*SSHHelperConfig, error) { + root, err := hcl.Parse(string(contents)) + if err != nil { + return nil, fmt.Errorf("ssh_helper: error parsing config: %s", err) + } + + list, ok := root.Node.(*ast.ObjectList) + if !ok { + return nil, fmt.Errorf("ssh_helper: error parsing config: file doesn't contain a root object") + } + + valid := []string{ + "vault_addr", + "ssh_mount_point", + "ca_cert", + "ca_path", + "allowed_cidr_list", + "allowed_roles", + "tls_skip_verify", + "tls_server_name", + } + if err := checkHCLKeys(list, valid); err != nil { + return nil, multierror.Prefix(err, "ssh_helper:") + } + + var c SSHHelperConfig + c.SSHMountPoint = SSHHelperDefaultMountPoint + if err := hcl.DecodeObject(&c, list); err != nil { + return nil, multierror.Prefix(err, "ssh_helper:") + } + + if c.VaultAddr == "" { + return nil, fmt.Errorf("ssh_helper: missing config 'vault_addr'") + } + return &c, nil +} + +// SSHHelper creates an SSHHelper object which can talk to Vault server with SSH backend +// mounted at default path ("ssh"). +func (c *Client) SSHHelper() *SSHHelper { + return c.SSHHelperWithMountPoint(SSHHelperDefaultMountPoint) +} + +// SSHHelperWithMountPoint creates an SSHHelper object which can talk to Vault server with SSH backend +// mounted at a specific mount point. +func (c *Client) SSHHelperWithMountPoint(mountPoint string) *SSHHelper { + return &SSHHelper{ + c: c, + MountPoint: mountPoint, + } +} + +// Verify verifies if the key provided by user is present in Vault server. The response +// will contain the IP address and username associated with the OTP. In case the +// OTP matches the echo request message, instead of searching an entry for the OTP, +// an echo response message is returned. This feature is used by ssh-helper to verify if +// its configured correctly. +func (c *SSHHelper) Verify(otp string) (*SSHVerifyResponse, error) { + data := map[string]interface{}{ + "otp": otp, + } + verifyPath := fmt.Sprintf("/v1/%s/verify", c.MountPoint) + r := c.c.NewRequest("PUT", verifyPath) + if err := r.SetJSONBody(data); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + secret, err := ParseSecret(resp.Body) + if err != nil { + return nil, err + } + + if secret.Data == nil { + return nil, nil + } + + var verifyResp SSHVerifyResponse + err = mapstructure.Decode(secret.Data, &verifyResp) + if err != nil { + return nil, err + } + return &verifyResp, nil +} + +func checkHCLKeys(node ast.Node, valid []string) error { + var list *ast.ObjectList + switch n := node.(type) { + case *ast.ObjectList: + list = n + case *ast.ObjectType: + list = n.List + default: + return fmt.Errorf("cannot check HCL keys of type %T", n) + } + + validMap := make(map[string]struct{}, len(valid)) + for _, v := range valid { + validMap[v] = struct{}{} + } + + var result error + for _, item := range list.Items { + key := item.Keys[0].Token.Value().(string) + if _, ok := validMap[key]; !ok { + result = multierror.Append(result, fmt.Errorf( + "invalid key '%s' on line %d", key, item.Assign.Line)) + } + } + + return result +} diff --git a/vendor/github.com/hashicorp/vault/api/sys.go b/vendor/github.com/hashicorp/vault/api/sys.go new file mode 100644 index 000000000..5fb111887 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys.go @@ -0,0 +1,11 @@ +package api + +// Sys is used to perform system-related operations on Vault. +type Sys struct { + c *Client +} + +// Sys is used to return the client for sys-related API calls. +func (c *Client) Sys() *Sys { + return &Sys{c: c} +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_audit.go b/vendor/github.com/hashicorp/vault/api/sys_audit.go new file mode 100644 index 000000000..1ffdef880 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_audit.go @@ -0,0 +1,114 @@ +package api + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +func (c *Sys) AuditHash(path string, input string) (string, error) { + body := map[string]interface{}{ + "input": input, + } + + r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/sys/audit-hash/%s", path)) + if err := r.SetJSONBody(body); err != nil { + return "", err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return "", err + } + defer resp.Body.Close() + + type d struct { + Hash string `json:"hash"` + } + + var result d + err = resp.DecodeJSON(&result) + if err != nil { + return "", err + } + + return result.Hash, err +} + +func (c *Sys) ListAudit() (map[string]*Audit, error) { + r := c.c.NewRequest("GET", "/v1/sys/audit") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result map[string]interface{} + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + + mounts := map[string]*Audit{} + for k, v := range result { + switch v.(type) { + case map[string]interface{}: + default: + continue + } + var res Audit + err = mapstructure.Decode(v, &res) + if err != nil { + return nil, err + } + // Not a mount, some other api.Secret data + if res.Type == "" { + continue + } + mounts[k] = &res + } + + return mounts, nil +} + +func (c *Sys) EnableAudit( + path string, auditType string, desc string, opts map[string]string) error { + body := map[string]interface{}{ + "type": auditType, + "description": desc, + "options": opts, + } + + r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/sys/audit/%s", path)) + if err := r.SetJSONBody(body); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +func (c *Sys) DisableAudit(path string) error { + r := c.c.NewRequest("DELETE", fmt.Sprintf("/v1/sys/audit/%s", path)) + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +// Structures for the requests/resposne are all down here. They aren't +// individually documented because the map almost directly to the raw HTTP API +// documentation. Please refer to that documentation for more details. + +type Audit struct { + Path string + Type string + Description string + Options map[string]string +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_auth.go b/vendor/github.com/hashicorp/vault/api/sys_auth.go new file mode 100644 index 000000000..1940e8417 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_auth.go @@ -0,0 +1,87 @@ +package api + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +func (c *Sys) ListAuth() (map[string]*AuthMount, error) { + r := c.c.NewRequest("GET", "/v1/sys/auth") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result map[string]interface{} + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + + mounts := map[string]*AuthMount{} + for k, v := range result { + switch v.(type) { + case map[string]interface{}: + default: + continue + } + var res AuthMount + err = mapstructure.Decode(v, &res) + if err != nil { + return nil, err + } + // Not a mount, some other api.Secret data + if res.Type == "" { + continue + } + mounts[k] = &res + } + + return mounts, nil +} + +func (c *Sys) EnableAuth(path, authType, desc string) error { + body := map[string]string{ + "type": authType, + "description": desc, + } + + r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/auth/%s", path)) + if err := r.SetJSONBody(body); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +func (c *Sys) DisableAuth(path string) error { + r := c.c.NewRequest("DELETE", fmt.Sprintf("/v1/sys/auth/%s", path)) + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +// Structures for the requests/resposne are all down here. They aren't +// individually documentd because the map almost directly to the raw HTTP API +// documentation. Please refer to that documentation for more details. + +type AuthMount struct { + Type string `json:"type" structs:"type" mapstructure:"type"` + Description string `json:"description" structs:"description" mapstructure:"description"` + Config AuthConfigOutput `json:"config" structs:"config" mapstructure:"config"` +} + +type AuthConfigOutput struct { + DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_capabilities.go b/vendor/github.com/hashicorp/vault/api/sys_capabilities.go new file mode 100644 index 000000000..80f621884 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_capabilities.go @@ -0,0 +1,43 @@ +package api + +import "fmt" + +func (c *Sys) CapabilitiesSelf(path string) ([]string, error) { + return c.Capabilities(c.c.Token(), path) +} + +func (c *Sys) Capabilities(token, path string) ([]string, error) { + body := map[string]string{ + "token": token, + "path": path, + } + + reqPath := "/v1/sys/capabilities" + if token == c.c.Token() { + reqPath = fmt.Sprintf("%s-self", reqPath) + } + + r := c.c.NewRequest("POST", reqPath) + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result map[string]interface{} + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + + var capabilities []string + capabilitiesRaw := result["capabilities"].([]interface{}) + for _, capability := range capabilitiesRaw { + capabilities = append(capabilities, capability.(string)) + } + return capabilities, nil +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_generate_root.go b/vendor/github.com/hashicorp/vault/api/sys_generate_root.go new file mode 100644 index 000000000..8dc2095f3 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_generate_root.go @@ -0,0 +1,77 @@ +package api + +func (c *Sys) GenerateRootStatus() (*GenerateRootStatusResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/generate-root/attempt") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result GenerateRootStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) GenerateRootInit(otp, pgpKey string) (*GenerateRootStatusResponse, error) { + body := map[string]interface{}{ + "otp": otp, + "pgp_key": pgpKey, + } + + r := c.c.NewRequest("PUT", "/v1/sys/generate-root/attempt") + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result GenerateRootStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) GenerateRootCancel() error { + r := c.c.NewRequest("DELETE", "/v1/sys/generate-root/attempt") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) GenerateRootUpdate(shard, nonce string) (*GenerateRootStatusResponse, error) { + body := map[string]interface{}{ + "key": shard, + "nonce": nonce, + } + + r := c.c.NewRequest("PUT", "/v1/sys/generate-root/update") + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result GenerateRootStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +type GenerateRootStatusResponse struct { + Nonce string + Started bool + Progress int + Required int + Complete bool + EncodedRootToken string `json:"encoded_root_token"` + PGPFingerprint string `json:"pgp_fingerprint"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_init.go b/vendor/github.com/hashicorp/vault/api/sys_init.go new file mode 100644 index 000000000..f824ab7dd --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_init.go @@ -0,0 +1,54 @@ +package api + +func (c *Sys) InitStatus() (bool, error) { + r := c.c.NewRequest("GET", "/v1/sys/init") + resp, err := c.c.RawRequest(r) + if err != nil { + return false, err + } + defer resp.Body.Close() + + var result InitStatusResponse + err = resp.DecodeJSON(&result) + return result.Initialized, err +} + +func (c *Sys) Init(opts *InitRequest) (*InitResponse, error) { + r := c.c.NewRequest("PUT", "/v1/sys/init") + if err := r.SetJSONBody(opts); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result InitResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +type InitRequest struct { + SecretShares int `json:"secret_shares"` + SecretThreshold int `json:"secret_threshold"` + StoredShares int `json:"stored_shares"` + PGPKeys []string `json:"pgp_keys"` + RecoveryShares int `json:"recovery_shares"` + RecoveryThreshold int `json:"recovery_threshold"` + RecoveryPGPKeys []string `json:"recovery_pgp_keys"` + RootTokenPGPKey string `json:"root_token_pgp_key"` +} + +type InitStatusResponse struct { + Initialized bool +} + +type InitResponse struct { + Keys []string `json:"keys"` + KeysB64 []string `json:"keys_base64"` + RecoveryKeys []string `json:"recovery_keys"` + RecoveryKeysB64 []string `json:"recovery_keys_base64"` + RootToken string `json:"root_token"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_leader.go b/vendor/github.com/hashicorp/vault/api/sys_leader.go new file mode 100644 index 000000000..201ac732e --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_leader.go @@ -0,0 +1,20 @@ +package api + +func (c *Sys) Leader() (*LeaderResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/leader") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result LeaderResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +type LeaderResponse struct { + HAEnabled bool `json:"ha_enabled"` + IsSelf bool `json:"is_self"` + LeaderAddress string `json:"leader_address"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_lease.go b/vendor/github.com/hashicorp/vault/api/sys_lease.go new file mode 100644 index 000000000..e5c19c42c --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_lease.go @@ -0,0 +1,48 @@ +package api + +func (c *Sys) Renew(id string, increment int) (*Secret, error) { + r := c.c.NewRequest("PUT", "/v1/sys/renew") + + body := map[string]interface{}{ + "increment": increment, + "lease_id": id, + } + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + +func (c *Sys) Revoke(id string) error { + r := c.c.NewRequest("PUT", "/v1/sys/revoke/"+id) + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) RevokePrefix(id string) error { + r := c.c.NewRequest("PUT", "/v1/sys/revoke-prefix/"+id) + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) RevokeForce(id string) error { + r := c.c.NewRequest("PUT", "/v1/sys/revoke-force/"+id) + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_mounts.go b/vendor/github.com/hashicorp/vault/api/sys_mounts.go new file mode 100644 index 000000000..ca5e42707 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_mounts.go @@ -0,0 +1,142 @@ +package api + +import ( + "fmt" + + "github.com/fatih/structs" + "github.com/mitchellh/mapstructure" +) + +func (c *Sys) ListMounts() (map[string]*MountOutput, error) { + r := c.c.NewRequest("GET", "/v1/sys/mounts") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result map[string]interface{} + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + + mounts := map[string]*MountOutput{} + for k, v := range result { + switch v.(type) { + case map[string]interface{}: + default: + continue + } + var res MountOutput + err = mapstructure.Decode(v, &res) + if err != nil { + return nil, err + } + // Not a mount, some other api.Secret data + if res.Type == "" { + continue + } + mounts[k] = &res + } + + return mounts, nil +} + +func (c *Sys) Mount(path string, mountInfo *MountInput) error { + body := structs.Map(mountInfo) + + r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/mounts/%s", path)) + if err := r.SetJSONBody(body); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +func (c *Sys) Unmount(path string) error { + r := c.c.NewRequest("DELETE", fmt.Sprintf("/v1/sys/mounts/%s", path)) + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) Remount(from, to string) error { + body := map[string]interface{}{ + "from": from, + "to": to, + } + + r := c.c.NewRequest("POST", "/v1/sys/remount") + if err := r.SetJSONBody(body); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) TuneMount(path string, config MountConfigInput) error { + body := structs.Map(config) + r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/mounts/%s/tune", path)) + if err := r.SetJSONBody(body); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) MountConfig(path string) (*MountConfigOutput, error) { + r := c.c.NewRequest("GET", fmt.Sprintf("/v1/sys/mounts/%s/tune", path)) + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result MountConfigOutput + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + + return &result, err +} + +type MountInput struct { + Type string `json:"type" structs:"type"` + Description string `json:"description" structs:"description"` + Config MountConfigInput `json:"config" structs:"config"` +} + +type MountConfigInput struct { + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` +} + +type MountOutput struct { + Type string `json:"type" structs:"type"` + Description string `json:"description" structs:"description"` + Config MountConfigOutput `json:"config" structs:"config"` +} + +type MountConfigOutput struct { + DefaultLeaseTTL int `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL int `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_policy.go b/vendor/github.com/hashicorp/vault/api/sys_policy.go new file mode 100644 index 000000000..ba0e17fab --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_policy.go @@ -0,0 +1,95 @@ +package api + +import "fmt" + +func (c *Sys) ListPolicies() ([]string, error) { + r := c.c.NewRequest("GET", "/v1/sys/policy") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result map[string]interface{} + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + + var ok bool + if _, ok = result["policies"]; !ok { + return nil, fmt.Errorf("policies not found in response") + } + + listRaw := result["policies"].([]interface{}) + var policies []string + + for _, val := range listRaw { + policies = append(policies, val.(string)) + } + + return policies, err +} + +func (c *Sys) GetPolicy(name string) (string, error) { + r := c.c.NewRequest("GET", fmt.Sprintf("/v1/sys/policy/%s", name)) + resp, err := c.c.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + if resp.StatusCode == 404 { + return "", nil + } + } + if err != nil { + return "", err + } + + var result map[string]interface{} + err = resp.DecodeJSON(&result) + if err != nil { + return "", err + } + + var ok bool + if _, ok = result["rules"]; !ok { + return "", fmt.Errorf("rules not found in response") + } + + return result["rules"].(string), nil +} + +func (c *Sys) PutPolicy(name, rules string) error { + body := map[string]string{ + "rules": rules, + } + + r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/sys/policy/%s", name)) + if err := r.SetJSONBody(body); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +func (c *Sys) DeletePolicy(name string) error { + r := c.c.NewRequest("DELETE", fmt.Sprintf("/v1/sys/policy/%s", name)) + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +type getPoliciesResp struct { + Rules string `json:"rules"` +} + +type listPoliciesResp struct { + Policies []string `json:"policies"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_rekey.go b/vendor/github.com/hashicorp/vault/api/sys_rekey.go new file mode 100644 index 000000000..e6d039e27 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_rekey.go @@ -0,0 +1,202 @@ +package api + +func (c *Sys) RekeyStatus() (*RekeyStatusResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/rekey/init") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyRecoveryKeyStatus() (*RekeyStatusResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/rekey-recovery-key/init") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyInit(config *RekeyInitRequest) (*RekeyStatusResponse, error) { + r := c.c.NewRequest("PUT", "/v1/sys/rekey/init") + if err := r.SetJSONBody(config); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyRecoveryKeyInit(config *RekeyInitRequest) (*RekeyStatusResponse, error) { + r := c.c.NewRequest("PUT", "/v1/sys/rekey-recovery-key/init") + if err := r.SetJSONBody(config); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyCancel() error { + r := c.c.NewRequest("DELETE", "/v1/sys/rekey/init") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) RekeyRecoveryKeyCancel() error { + r := c.c.NewRequest("DELETE", "/v1/sys/rekey-recovery-key/init") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) RekeyUpdate(shard, nonce string) (*RekeyUpdateResponse, error) { + body := map[string]interface{}{ + "key": shard, + "nonce": nonce, + } + + r := c.c.NewRequest("PUT", "/v1/sys/rekey/update") + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyUpdateResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyRecoveryKeyUpdate(shard, nonce string) (*RekeyUpdateResponse, error) { + body := map[string]interface{}{ + "key": shard, + "nonce": nonce, + } + + r := c.c.NewRequest("PUT", "/v1/sys/rekey-recovery-key/update") + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyUpdateResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyRetrieveBackup() (*RekeyRetrieveResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/rekey/backup") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyRetrieveResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyRetrieveRecoveryBackup() (*RekeyRetrieveResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/rekey/recovery-backup") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result RekeyRetrieveResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +func (c *Sys) RekeyDeleteBackup() error { + r := c.c.NewRequest("DELETE", "/v1/sys/rekey/backup") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + + return err +} + +func (c *Sys) RekeyDeleteRecoveryBackup() error { + r := c.c.NewRequest("DELETE", "/v1/sys/rekey/recovery-backup") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + + return err +} + +type RekeyInitRequest struct { + SecretShares int `json:"secret_shares"` + SecretThreshold int `json:"secret_threshold"` + PGPKeys []string `json:"pgp_keys"` + Backup bool +} + +type RekeyStatusResponse struct { + Nonce string + Started bool + T int + N int + Progress int + Required int + PGPFingerprints []string `json:"pgp_fingerprints"` + Backup bool +} + +type RekeyUpdateResponse struct { + Nonce string + Complete bool + Keys []string + KeysB64 []string `json:"keys_base64"` + PGPFingerprints []string `json:"pgp_fingerprints"` + Backup bool +} + +type RekeyRetrieveResponse struct { + Nonce string + Keys map[string][]string + KeysB64 map[string][]string `json:"keys_base64"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_rotate.go b/vendor/github.com/hashicorp/vault/api/sys_rotate.go new file mode 100644 index 000000000..8108dced8 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_rotate.go @@ -0,0 +1,30 @@ +package api + +import "time" + +func (c *Sys) Rotate() error { + r := c.c.NewRequest("POST", "/v1/sys/rotate") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) KeyStatus() (*KeyStatus, error) { + r := c.c.NewRequest("GET", "/v1/sys/key-status") + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + result := new(KeyStatus) + err = resp.DecodeJSON(result) + return result, err +} + +type KeyStatus struct { + Term int `json:"term"` + InstallTime time.Time `json:"install_time"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_seal.go b/vendor/github.com/hashicorp/vault/api/sys_seal.go new file mode 100644 index 000000000..b80e33a94 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_seal.go @@ -0,0 +1,59 @@ +package api + +func (c *Sys) SealStatus() (*SealStatusResponse, error) { + r := c.c.NewRequest("GET", "/v1/sys/seal-status") + return sealStatusRequest(c, r) +} + +func (c *Sys) Seal() error { + r := c.c.NewRequest("PUT", "/v1/sys/seal") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} + +func (c *Sys) ResetUnsealProcess() (*SealStatusResponse, error) { + body := map[string]interface{}{"reset": true} + + r := c.c.NewRequest("PUT", "/v1/sys/unseal") + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + return sealStatusRequest(c, r) +} + +func (c *Sys) Unseal(shard string) (*SealStatusResponse, error) { + body := map[string]interface{}{"key": shard} + + r := c.c.NewRequest("PUT", "/v1/sys/unseal") + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + + return sealStatusRequest(c, r) +} + +func sealStatusRequest(c *Sys, r *Request) (*SealStatusResponse, error) { + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result SealStatusResponse + err = resp.DecodeJSON(&result) + return &result, err +} + +type SealStatusResponse struct { + Sealed bool `json:"sealed"` + T int `json:"t"` + N int `json:"n"` + Progress int `json:"progress"` + Version string `json:"version"` + ClusterName string `json:"cluster_name,omitempty"` + ClusterID string `json:"cluster_id,omitempty"` +} diff --git a/vendor/github.com/hashicorp/vault/api/sys_stepdown.go b/vendor/github.com/hashicorp/vault/api/sys_stepdown.go new file mode 100644 index 000000000..421e5f19f --- /dev/null +++ b/vendor/github.com/hashicorp/vault/api/sys_stepdown.go @@ -0,0 +1,10 @@ +package api + +func (c *Sys) StepDown() error { + r := c.c.NewRequest("PUT", "/v1/sys/step-down") + resp, err := c.c.RawRequest(r) + if err == nil { + defer resp.Body.Close() + } + return err +} diff --git a/vendor/github.com/sethgrid/pester/LICENSE.md b/vendor/github.com/sethgrid/pester/LICENSE.md new file mode 100644 index 000000000..4b49dda30 --- /dev/null +++ b/vendor/github.com/sethgrid/pester/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) SendGrid 2016 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/sethgrid/pester/README.md b/vendor/github.com/sethgrid/pester/README.md new file mode 100644 index 000000000..e41f4d6f6 --- /dev/null +++ b/vendor/github.com/sethgrid/pester/README.md @@ -0,0 +1,126 @@ +# pester + +`pester` wraps Go's standard lib http client to provide several options to increase resiliency in your request. If you experience poor network conditions or requests could experience varied delays, you can now pester the endpoint for data. +- Send out multiple requests and get the first back (only used for GET calls) +- Retry on errors +- Backoff + +### Simple Example +Use `pester` where you would use the http client calls. By default, pester will use a concurrency of 1, and retry the endpoint 3 times with the `DefaultBackoff` strategy of waiting 1 second between retries. +```go +/* swap in replacement, just switch + http.{Get|Post|PostForm|Head|Do} to + pester.{Get|Post|PostForm|Head|Do} +*/ +resp, err := pester.Get("http://sethammons.com") +``` + +### Backoff Strategy +Provide your own backoff strategy, or use one of the provided built in strategies: +- `DefaultBackoff`: 1 second +- `LinearBackoff`: n seconds where n is the retry number +- `LinearJitterBackoff`: n seconds where n is the retry number, +/- 0-33% +- `ExponentialBackoff`: n seconds where n is 2^(retry number) +- `ExponentialJitterBackoff`: n seconds where n is 2^(retry number), +/- 0-33% + +```go +client := pester.New() +client.Backoff = func(retry int) time.Duration { + // set up something dynamic or use a look up table + return time.Duration(retry) * time.Minute +} +``` + +### Complete example +For a complete and working example, see the sample directory. +`pester` allows you to use a constructor to control: +- backoff strategy +- reties +- concurrency +- keeping a log for debugging +```go +package main + +import ( + "log" + "net/http" + "strings" + + "github.com/sethgrid/pester" +) + +func main() { + log.Println("Starting...") + + { // drop in replacement for http.Get and other client methods + resp, err := pester.Get("http://example.com") + if err != nil { + log.Println("error GETing example.com", err) + } + defer resp.Body.Close() + log.Printf("example.com %s", resp.Status) + } + + { // control the resiliency + client := pester.New() + client.Concurrency = 3 + client.MaxRetries = 5 + client.Backoff = pester.ExponentialBackoff + client.KeepLog = true + + resp, err := client.Get("http://example.com") + if err != nil { + log.Println("error GETing example.com", client.LogString()) + } + defer resp.Body.Close() + log.Printf("example.com %s", resp.Status) + } + + { // use the pester version of http.Client.Do + req, err := http.NewRequest("POST", "http://example.com", strings.NewReader("data")) + if err != nil { + log.Fatal("Unable to create a new http request", err) + } + resp, err := pester.Do(req) + if err != nil { + log.Println("error POSTing example.com", err) + } + defer resp.Body.Close() + log.Printf("example.com %s", resp.Status) + } +} + +``` + +### Example Log +`pester` also allows you to control the resiliency and can optionally log the errors. +```go +c := pester.New() +c.KeepLog = true + +nonExistantURL := "http://localhost:9000/foo" +_, _ = c.Get(nonExistantURL) + +fmt.Println(c.LogString()) +/* +Output: + +1432402837 Get [GET] http://localhost:9000/foo request-0 retry-0 error: Get http://localhost:9000/foo: dial tcp 127.0.0.1:9000: connection refused +1432402838 Get [GET] http://localhost:9000/foo request-0 retry-1 error: Get http://localhost:9000/foo: dial tcp 127.0.0.1:9000: connection refused +1432402839 Get [GET] http://localhost:9000/foo request-0 retry-2 error: Get http://localhost:9000/foo: dial tcp 127.0.0.1:9000: connection refused +*/ +``` + +### Tests + +You can run tests in the root directory with `$ go test`. There is a benchmark-like test available with `$ cd benchmarks; go test`. +You can see `pester` in action with `$ cd sample; go run main.go`. + +For watching open file descriptors, you can run `watch "lsof -i -P | grep main"` if you started the app with `go run main.go`. +I did this for watching for FD leaks. My method was to alter `sample/main.go` to only run one case (`pester.Get with set backoff stategy, concurrency and retries increased`) +and adding a sleep after the result came back. This let me verify if FDs were getting left open when they should have closed. If you know a better way, let me know! +I was able to see that FDs are now closing when they should :) + +![Are we there yet?](http://butchbellah.com/wp-content/uploads/2012/06/Are-We-There-Yet.jpg) + +Are we there yet? Are we there yet? Are we there yet? Are we there yet? ... diff --git a/vendor/github.com/sethgrid/pester/main.go b/vendor/github.com/sethgrid/pester/main.go new file mode 100644 index 000000000..8eb91fe52 --- /dev/null +++ b/vendor/github.com/sethgrid/pester/main.go @@ -0,0 +1,423 @@ +package pester + +// pester provides additional resiliency over the standard http client methods by +// allowing you to control concurrency, retries, and a backoff strategy. + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "net/http" + "net/url" + "sync" + "time" +) + +// Client wraps the http client and exposes all the functionality of the http.Client. +// Additionally, Client provides pester specific values for handling resiliency. +type Client struct { + // wrap it to provide access to http built ins + hc *http.Client + + Transport http.RoundTripper + CheckRedirect func(req *http.Request, via []*http.Request) error + Jar http.CookieJar + Timeout time.Duration + + // pester specific + Concurrency int + MaxRetries int + Backoff BackoffStrategy + KeepLog bool + + SuccessReqNum int + SuccessRetryNum int + + wg *sync.WaitGroup + + sync.Mutex + ErrLog []ErrEntry +} + +// ErrEntry is used to provide the LogString() data and is populated +// each time an error happens if KeepLog is set. +// ErrEntry.Retry is deprecated in favor of ErrEntry.Attempt +type ErrEntry struct { + Time time.Time + Method string + URL string + Verb string + Request int + Retry int + Attempt int + Err error +} + +// result simplifies the channel communication for concurrent request handling +type result struct { + resp *http.Response + err error + req int + retry int +} + +// params represents all the params needed to run http client calls and pester errors +type params struct { + method string + verb string + req *http.Request + url string + bodyType string + body io.Reader + data url.Values +} + +// New constructs a new DefaultClient with sensible default values +func New() *Client { + return &Client{ + Concurrency: DefaultClient.Concurrency, + MaxRetries: DefaultClient.MaxRetries, + Backoff: DefaultClient.Backoff, + ErrLog: DefaultClient.ErrLog, + wg: &sync.WaitGroup{}, + } +} + +// NewExtendedClient allows you to pass in an http.Client that is previously set up +// and extends it to have Pester's features of concurrency and retries. +func NewExtendedClient(hc *http.Client) *Client { + c := New() + c.hc = hc + return c +} + +// BackoffStrategy is used to determine how long a retry request should wait until attempted +type BackoffStrategy func(retry int) time.Duration + +// DefaultClient provides sensible defaults +var DefaultClient = &Client{Concurrency: 1, MaxRetries: 3, Backoff: DefaultBackoff, ErrLog: []ErrEntry{}} + +// DefaultBackoff always returns 1 second +func DefaultBackoff(_ int) time.Duration { + return 1 * time.Second +} + +// ExponentialBackoff returns ever increasing backoffs by a power of 2 +func ExponentialBackoff(i int) time.Duration { + return time.Duration(math.Pow(2, float64(i))) * time.Second +} + +// ExponentialJitterBackoff returns ever increasing backoffs by a power of 2 +// with +/- 0-33% to prevent sychronized reuqests. +func ExponentialJitterBackoff(i int) time.Duration { + return jitter(int(math.Pow(2, float64(i)))) +} + +// LinearBackoff returns increasing durations, each a second longer than the last +func LinearBackoff(i int) time.Duration { + return time.Duration(i) * time.Second +} + +// LinearJitterBackoff returns increasing durations, each a second longer than the last +// with +/- 0-33% to prevent sychronized reuqests. +func LinearJitterBackoff(i int) time.Duration { + return jitter(i) +} + +// jitter keeps the +/- 0-33% logic in one place +func jitter(i int) time.Duration { + ms := i * 1000 + + maxJitter := ms / 3 + + rand.Seed(time.Now().Unix()) + jitter := rand.Intn(maxJitter + 1) + + if rand.Intn(2) == 1 { + ms = ms + jitter + } else { + ms = ms - jitter + } + + // a jitter of 0 messes up the time.Tick chan + if ms <= 0 { + ms = 1 + } + + return time.Duration(ms) * time.Millisecond +} + +// Wait blocks until all pester requests have returned +// Probably not that useful outside of testing. +func (c *Client) Wait() { + c.wg.Wait() +} + +// pester provides all the logic of retries, concurrency, backoff, and logging +func (c *Client) pester(p params) (*http.Response, error) { + resultCh := make(chan result) + multiplexCh := make(chan result) + finishCh := make(chan struct{}) + + // track all requests that go out so we can close the late listener routine that closes late incoming response bodies + totalSentRequests := &sync.WaitGroup{} + totalSentRequests.Add(1) + defer totalSentRequests.Done() + allRequestsBackCh := make(chan struct{}) + go func() { + totalSentRequests.Wait() + close(allRequestsBackCh) + }() + + // GET calls should be idempotent and can make use + // of concurrency. Other verbs can mutate and should not + // make use of the concurrency feature + concurrency := c.Concurrency + if p.verb != "GET" { + concurrency = 1 + } + + c.Lock() + if c.hc == nil { + c.hc = &http.Client{} + c.hc.Transport = c.Transport + c.hc.CheckRedirect = c.CheckRedirect + c.hc.Jar = c.Jar + c.hc.Timeout = c.Timeout + } + c.Unlock() + + // re-create the http client so we can leverage the std lib + httpClient := http.Client{ + Transport: c.hc.Transport, + CheckRedirect: c.hc.CheckRedirect, + Jar: c.hc.Jar, + Timeout: c.hc.Timeout, + } + + // if we have a request body, we need to save it for later + var originalRequestBody []byte + var originalBody []byte + var err error + if p.req != nil && p.req.Body != nil { + originalRequestBody, err = ioutil.ReadAll(p.req.Body) + if err != nil { + return &http.Response{}, errors.New("error reading request body") + } + p.req.Body.Close() + } + if p.body != nil { + originalBody, err = ioutil.ReadAll(p.body) + if err != nil { + return &http.Response{}, errors.New("error reading body") + } + } + + AttemptLimit := c.MaxRetries + if AttemptLimit <= 0 { + AttemptLimit = 1 + } + + for req := 0; req < concurrency; req++ { + c.wg.Add(1) + totalSentRequests.Add(1) + go func(n int, p params) { + defer c.wg.Done() + defer totalSentRequests.Done() + + var err error + for i := 1; i <= AttemptLimit; i++ { + c.wg.Add(1) + defer c.wg.Done() + select { + case <-finishCh: + return + default: + } + resp := &http.Response{} + + // rehydrate the body (it is drained each read) + if len(originalRequestBody) > 0 { + p.req.Body = ioutil.NopCloser(bytes.NewBuffer(originalRequestBody)) + } + if len(originalBody) > 0 { + p.body = bytes.NewBuffer(originalBody) + } + + // route the calls + switch p.method { + case "Do": + resp, err = httpClient.Do(p.req) + case "Get": + resp, err = httpClient.Get(p.url) + case "Head": + resp, err = httpClient.Head(p.url) + case "Post": + resp, err = httpClient.Post(p.url, p.bodyType, p.body) + case "PostForm": + resp, err = httpClient.PostForm(p.url, p.data) + } + + // Early return if we have a valid result + // Only retry (ie, continue the loop) on 5xx status codes + if err == nil && resp.StatusCode < 500 { + multiplexCh <- result{resp: resp, err: err, req: n, retry: i} + return + } + + c.log(ErrEntry{ + Time: time.Now(), + Method: p.method, + Verb: p.verb, + URL: p.url, + Request: n, + Retry: i + 1, // would remove, but would break backward compatibility + Attempt: i, + Err: err, + }) + + // if it is the last iteration, grab the result (which is an error at this point) + if i == AttemptLimit { + multiplexCh <- result{resp: resp, err: err} + return + } + + // if we are retrying, we should close this response body to free the fd + if resp != nil { + resp.Body.Close() + } + + // prevent a 0 from causing the tick to block, pass additional microsecond + <-time.Tick(c.Backoff(i) + 1*time.Microsecond) + } + }(req, p) + } + + // spin off the go routine so it can continually listen in on late results and close the response bodies + go func() { + gotFirstResult := false + for { + select { + case res := <-multiplexCh: + if !gotFirstResult { + gotFirstResult = true + close(finishCh) + resultCh <- res + } else if res.resp != nil { + // we only return one result to the caller; close all other response bodies that come back + // drain the body before close as to not prevent keepalive. see https://gist.github.com/mholt/eba0f2cc96658be0f717 + io.Copy(ioutil.Discard, res.resp.Body) + res.resp.Body.Close() + } + case <-allRequestsBackCh: + // don't leave this goroutine running + return + } + } + }() + + select { + case res := <-resultCh: + c.Lock() + defer c.Unlock() + c.SuccessReqNum = res.req + c.SuccessRetryNum = res.retry + return res.resp, res.err + } +} + +// LogString provides a string representation of the errors the client has seen +func (c *Client) LogString() string { + c.Lock() + defer c.Unlock() + var res string + for _, e := range c.ErrLog { + res += fmt.Sprintf("%d %s [%s] %s request-%d retry-%d error: %s\n", + e.Time.Unix(), e.Method, e.Verb, e.URL, e.Request, e.Retry, e.Err) + } + return res +} + +// LogErrCount is a helper method used primarily for test validation +func (c *Client) LogErrCount() int { + c.Lock() + defer c.Unlock() + return len(c.ErrLog) +} + +// EmbedHTTPClient allows you to extend an existing Pester client with an +// underlying http.Client, such as https://godoc.org/golang.org/x/oauth2/google#DefaultClient +func (c *Client) EmbedHTTPClient(hc *http.Client) { + c.hc = hc +} + +func (c *Client) log(e ErrEntry) { + if c.KeepLog { + c.Lock() + c.ErrLog = append(c.ErrLog, e) + c.Unlock() + } +} + +// Do provides the same functionality as http.Client.Do +func (c *Client) Do(req *http.Request) (resp *http.Response, err error) { + return c.pester(params{method: "Do", req: req, verb: req.Method, url: req.URL.String()}) +} + +// Get provides the same functionality as http.Client.Get +func (c *Client) Get(url string) (resp *http.Response, err error) { + return c.pester(params{method: "Get", url: url, verb: "GET"}) +} + +// Head provides the same functionality as http.Client.Head +func (c *Client) Head(url string) (resp *http.Response, err error) { + return c.pester(params{method: "Head", url: url, verb: "HEAD"}) +} + +// Post provides the same functionality as http.Client.Post +func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + return c.pester(params{method: "Post", url: url, bodyType: bodyType, body: body, verb: "POST"}) +} + +// PostForm provides the same functionality as http.Client.PostForm +func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) { + return c.pester(params{method: "PostForm", url: url, data: data, verb: "POST"}) +} + +//////////////////////////////////////// +// Provide self-constructing variants // +//////////////////////////////////////// + +// Do provides the same functionality as http.Client.Do and creates its own constructor +func Do(req *http.Request) (resp *http.Response, err error) { + c := New() + return c.Do(req) +} + +// Get provides the same functionality as http.Client.Get and creates its own constructor +func Get(url string) (resp *http.Response, err error) { + c := New() + return c.Get(url) +} + +// Head provides the same functionality as http.Client.Head and creates its own constructor +func Head(url string) (resp *http.Response, err error) { + c := New() + return c.Head(url) +} + +// Post provides the same functionality as http.Client.Post and creates its own constructor +func Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + c := New() + return c.Post(url, bodyType, body) +} + +// PostForm provides the same functionality as http.Client.PostForm and creates its own constructor +func PostForm(url string, data url.Values) (resp *http.Response, err error) { + c := New() + return c.PostForm(url, data) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index cd6674c05..2e438732b 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1386,23 +1386,29 @@ "path": "github.com/hashicorp/serf/coordinate", "revision": "e4ec8cc423bbe20d26584b96efbeb9102e16d05f" }, + { + "checksumSHA1": "2fkVZIzvxIGBLhSiVnkTgGiqpQ4=", + "path": "github.com/hashicorp/vault/api", + "revision": "9a60bf2a50e4dd1ba4b929a3ccf8072435cbdd0a", + "revisionTime": "2016-10-29T21:01:49Z" + }, { "checksumSHA1": "ft77GtqeZEeCXioGpF/s6DlGm/U=", "path": "github.com/hashicorp/vault/helper/compressutil", - "revision": "7f8ac1fa8d42ce81855b6a763d137667fcd85244", - "revisionTime": "2016-10-20T16:39:19Z" + "revision": "9a60bf2a50e4dd1ba4b929a3ccf8072435cbdd0a", + "revisionTime": "2016-10-29T21:01:49Z" }, { "checksumSHA1": "yUiSTPf0QUuL2r/81sjuytqBoeQ=", "path": "github.com/hashicorp/vault/helper/jsonutil", - "revision": "7f8ac1fa8d42ce81855b6a763d137667fcd85244", - "revisionTime": "2016-10-20T16:39:19Z" + "revision": "9a60bf2a50e4dd1ba4b929a3ccf8072435cbdd0a", + "revisionTime": "2016-10-29T21:01:49Z" }, { "checksumSHA1": "YmXAnTwbzhLLBZM+1tQrJiG3qpc=", "path": "github.com/hashicorp/vault/helper/pgpkeys", - "revision": "7f8ac1fa8d42ce81855b6a763d137667fcd85244", - "revisionTime": "2016-10-20T16:39:19Z" + "revision": "9a60bf2a50e4dd1ba4b929a3ccf8072435cbdd0a", + "revisionTime": "2016-10-29T21:01:49Z" }, { "path": "github.com/hashicorp/yamux", From b2b5831205125a1c2bb6e7edca610d63eb584edc Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 30 Sep 2016 11:44:12 -0700 Subject: [PATCH 2/5] "vault" provider registration To reduce the risk of secret exposure via Terraform state and log output, we default to creating a relatively-short-lived token (20 minutes) such that Vault can, where possible, automatically revoke any retrieved secrets shortly after Terraform has finished running. This has some implications for usage of this provider that will be spelled out in more detail in the docs that will be added in a later commit, but the most significant implication is that a plan created by "terraform plan" that includes secrets leased from Vault must be *applied* before the lease period expires to ensure that the issued secrets remain valid. No resources yet. They will follow in subsequent commits. --- builtin/bins/provider-vault/main.go | 12 ++ builtin/providers/vault/provider.go | 155 +++++++++++++++++++++++ builtin/providers/vault/provider_test.go | 60 +++++++++ command/internal_plugin_list.go | 2 + 4 files changed, 229 insertions(+) create mode 100644 builtin/bins/provider-vault/main.go create mode 100644 builtin/providers/vault/provider.go create mode 100644 builtin/providers/vault/provider_test.go diff --git a/builtin/bins/provider-vault/main.go b/builtin/bins/provider-vault/main.go new file mode 100644 index 000000000..720a9a513 --- /dev/null +++ b/builtin/bins/provider-vault/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/vault" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: vault.Provider, + }) +} diff --git a/builtin/providers/vault/provider.go b/builtin/providers/vault/provider.go new file mode 100644 index 000000000..157299e3b --- /dev/null +++ b/builtin/providers/vault/provider.go @@ -0,0 +1,155 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_ADDR", nil), + Description: "URL of the root of the target Vault server.", + }, + "token": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_TOKEN", nil), + Description: "Token to use to authenticate to Vault.", + }, + "ca_cert_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ca_cert_dir"}, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CACERT", nil), + Description: "Path to a CA certificate file to validate the server's certificate.", + }, + "ca_cert_dir": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ca_cert_file"}, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CAPATH", nil), + Description: "Path to directory containing CA certificate files to validate the server's certificate.", + }, + "client_auth": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Client authentication credentials.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cert_file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CLIENT_CERT", nil), + Description: "Path to a file containing the client certificate.", + }, + "key_file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_CLIENT_KEY", nil), + Description: "Path to a file containing the private key that the certificate was issued for.", + }, + }, + }, + }, + "skip_tls_verify": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("VAULT_SKIP_VERIFY", nil), + Description: "Set this to true only if the target Vault server is an insecure development instance.", + }, + "max_lease_ttl_seconds": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + + // Default is 20min, which is intended to be enough time for + // a reasonable Terraform run can complete but not + // significantly longer, so that any leases are revoked shortly + // after Terraform has finished running. + DefaultFunc: schema.EnvDefaultFunc("TERRAFORM_VAULT_MAX_TTL", 1200), + + Description: "Maximum TTL for secret leases requested by this provider", + }, + }, + + ConfigureFunc: providerConfigure, + + ResourcesMap: map[string]*schema.Resource{ + }, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := &api.Config{ + Address: d.Get("address").(string), + } + + clientAuthI := d.Get("client_auth").([]interface{}) + if len(clientAuthI) > 1 { + return nil, fmt.Errorf("client_auth block may appear only once") + } + + clientAuthCert := "" + clientAuthKey := "" + if len(clientAuthI) == 1 { + clientAuth := clientAuthI[0].(map[string]interface{}) + clientAuthCert = clientAuth["cert_file"].(string) + clientAuthKey = clientAuth["key_file"].(string) + } + + config.ConfigureTLS(&api.TLSConfig{ + CACert: d.Get("ca_cert_file").(string), + CAPath: d.Get("ca_cert_dir").(string), + Insecure: d.Get("skip_tls_verify").(bool), + + ClientCert: clientAuthCert, + ClientKey: clientAuthKey, + }) + + client, err := api.NewClient(config) + if err != nil { + return nil, fmt.Errorf("failed to configure Vault API: %s", err) + } + + // In order to enforce our relatively-short lease TTL, we derive a + // temporary child token that inherits all of the policies of the + // token we were given but expires after max_lease_ttl_seconds. + // + // The intent here is that Terraform will need to re-fetch any + // secrets on each run and so we limit the exposure risk of secrets + // that end up stored in the Terraform state, assuming that they are + // credentials that Vault is able to revoke. + // + // Caution is still required with state files since not all secrets + // can explicitly be revoked, and this limited scope won't apply to + // any secrets that are *written* by Terraform to Vault. + + client.SetToken(d.Get("token").(string)) + renewable := false + childTokenLease, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + DisplayName: "terraform", + TTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds").(int)), + ExplicitMaxTTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds").(int)), + Renewable: &renewable, + }) + if err != nil { + return nil, fmt.Errorf("failed to create limited child token: %s", err) + } + + childToken := childTokenLease.Auth.ClientToken + policies := childTokenLease.Auth.Policies + + log.Printf("[INFO] Using Vault token with the following policies: %s", strings.Join(policies, ", ")) + + client.SetToken(childToken) + + return client, nil +} diff --git a/builtin/providers/vault/provider_test.go b/builtin/providers/vault/provider_test.go new file mode 100644 index 000000000..f26d163e1 --- /dev/null +++ b/builtin/providers/vault/provider_test.go @@ -0,0 +1,60 @@ +package vault + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// How to run the acceptance tests for this provider: +// +// - Obtain an official Vault release from the Vault website at +// https://vaultproject.io/ and extract the "vault" binary +// somewhere. +// +// - Run the following to start the Vault server in development mode: +// vault server -dev +// +// - Take the "Root Token" value printed by Vault as the server started +// up and set it as the value of the VAULT_TOKEN environment variable +// in a new shell whose current working directory is the root of the +// Terraform repository. +// +// - As directed by the Vault server output, set the VAULT_ADDR environment +// variable. e.g.: +// export VAULT_ADDR='http://127.0.0.1:8200' +// +// - Run the Terraform acceptance tests as usual: +// make testacc TEST=./builtin/providers/vault +// +// The tests expect to be run in a fresh, empty Vault and thus do not attempt +// to randomize or otherwise make the generated resource paths unique on +// each run. In case of weird behavior, restart the Vault dev server to +// start over with a fresh Vault. (Remember to reset VAULT_TOKEN.) + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +var testProvider *schema.Provider +var testProviders map[string]terraform.ResourceProvider + +func init() { + testProvider = Provider().(*schema.Provider) + testProviders = map[string]terraform.ResourceProvider{ + "vault": testProvider, + } +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("VAULT_ADDR"); v == "" { + t.Fatal("VAULT_ADDR must be set for acceptance tests") + } + if v := os.Getenv("VAULT_TOKEN"); v == "" { + t.Fatal("VAULT_TOKEN must be set for acceptance tests") + } +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index e40de2664..e808dc813 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -52,6 +52,7 @@ import ( tlsprovider "github.com/hashicorp/terraform/builtin/providers/tls" tritonprovider "github.com/hashicorp/terraform/builtin/providers/triton" ultradnsprovider "github.com/hashicorp/terraform/builtin/providers/ultradns" + vaultprovider "github.com/hashicorp/terraform/builtin/providers/vault" vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd" vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere" chefresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" @@ -110,6 +111,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "tls": tlsprovider.Provider, "triton": tritonprovider.Provider, "ultradns": ultradnsprovider.Provider, + "vault": vaultprovider.Provider, "vcd": vcdprovider.Provider, "vsphere": vsphereprovider.Provider, } From c1d1f902f565487a7fc031d1eea441ee6c5260ad Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 30 Sep 2016 15:12:54 -0700 Subject: [PATCH 3/5] provider/vault: vault_generic_secret resource This resource allows writing a generic secret, and indeed anything else that obeys the expected create/update/delete lifecycle, into vault via writes to its logical path namespace. --- builtin/providers/vault/provider.go | 1 + .../vault/resource_generic_secret.go | 87 ++++++++++++++ .../vault/resource_generic_secret_test.go | 106 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 builtin/providers/vault/resource_generic_secret.go create mode 100644 builtin/providers/vault/resource_generic_secret_test.go diff --git a/builtin/providers/vault/provider.go b/builtin/providers/vault/provider.go index 157299e3b..bbf391a80 100644 --- a/builtin/providers/vault/provider.go +++ b/builtin/providers/vault/provider.go @@ -83,6 +83,7 @@ func Provider() terraform.ResourceProvider { ConfigureFunc: providerConfigure, ResourcesMap: map[string]*schema.Resource{ + "vault_generic_secret": genericSecretResource(), }, } } diff --git a/builtin/providers/vault/resource_generic_secret.go b/builtin/providers/vault/resource_generic_secret.go new file mode 100644 index 000000000..a2a820c74 --- /dev/null +++ b/builtin/providers/vault/resource_generic_secret.go @@ -0,0 +1,87 @@ +package vault + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/hashicorp/vault/api" +) + +func genericSecretResource() *schema.Resource { + return &schema.Resource{ + Create: genericSecretResourceWrite, + Update: genericSecretResourceWrite, + Delete: genericSecretResourceDelete, + Read: genericSecretResourceRead, + + Schema: map[string]*schema.Schema{ + "path": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Full path where the generic secret will be written.", + }, + + // Data is passed as JSON so that an arbitrary structure is + // possible, rather than forcing e.g. all values to be strings. + "data_json": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "JSON-encoded secret data to write.", + }, + }, + } +} + +func genericSecretResourceWrite(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Get("path").(string) + + var data map[string]interface{} + err := json.Unmarshal([]byte(d.Get("data_json").(string)), &data) + if err != nil { + return fmt.Errorf("data_json %#v syntax error: %s", d.Get("data_json"), err) + } + + log.Printf("[DEBUG] Writing generic Vault secret to %s", path) + _, err = client.Logical().Write(path, data) + if err != nil { + return fmt.Errorf("error writing to Vault: %s", err) + } + + d.SetId(path) + + return nil +} + +func genericSecretResourceDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + + log.Printf("[DEBUG] Deleting generic Vault from %s", path) + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting from Vault: %s", err) + } + + return nil +} + +func genericSecretResourceRead(d *schema.ResourceData, meta interface{}) error { + // We don't actually attempt to read back the secret data + // here, so that Terraform can be configured with a token + // that has only write access to the relevant part of the + // store. + // + // This means that Terraform cannot detect drift for + // generic secrets, but detecting drift seems less important + // than being able to limit the effect of exposure of + // Terraform's Vault token. + log.Printf("[WARN] vault_generic_secret does not automatically refresh") + return nil +} diff --git a/builtin/providers/vault/resource_generic_secret_test.go b/builtin/providers/vault/resource_generic_secret_test.go new file mode 100644 index 000000000..7636565cd --- /dev/null +++ b/builtin/providers/vault/resource_generic_secret_test.go @@ -0,0 +1,106 @@ +package vault + +import ( + "fmt" + "testing" + + r "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/hashicorp/vault/api" +) + +func TestResourceGenericSecret(t *testing.T) { + r.Test(t, r.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []r.TestStep{ + r.TestStep{ + Config: testResourceGenericSecret_initialConfig, + Check: testResourceGenericSecret_initialCheck, + }, + r.TestStep{ + Config: testResourceGenericSecret_updateConfig, + Check: testResourceGenericSecret_updateCheck, + }, + }, + }) +} + +var testResourceGenericSecret_initialConfig = ` + +resource "vault_generic_secret" "test" { + path = "secret/foo" + data_json = < Date: Fri, 30 Sep 2016 16:20:08 -0700 Subject: [PATCH 4/5] provider/vault: vault_generic_secret data source --- .../vault/data_source_generic_secret.go | 106 ++++++++++++++++++ .../vault/data_source_generic_secret_test.go | 62 ++++++++++ builtin/providers/vault/provider.go | 4 + 3 files changed, 172 insertions(+) create mode 100644 builtin/providers/vault/data_source_generic_secret.go create mode 100644 builtin/providers/vault/data_source_generic_secret_test.go diff --git a/builtin/providers/vault/data_source_generic_secret.go b/builtin/providers/vault/data_source_generic_secret.go new file mode 100644 index 000000000..b99f8b0fa --- /dev/null +++ b/builtin/providers/vault/data_source_generic_secret.go @@ -0,0 +1,106 @@ +package vault + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/hashicorp/vault/api" +) + +func genericSecretDataSource() *schema.Resource { + return &schema.Resource{ + Read: genericSecretDataSourceRead, + + Schema: map[string]*schema.Schema{ + "path": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Full path from which a secret will be read.", + }, + + "data_json": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "JSON-encoded secret data read from Vault.", + }, + + "data": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + Description: "Map of strings read from Vault.", + }, + + "lease_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "Lease identifier assigned by vault.", + }, + + "lease_duration": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: "Lease duration in seconds relative to the time in lease_start_time.", + }, + + "lease_start_time": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "Time at which the lease was read, using the clock of the system where Terraform was running", + }, + + "lease_renewable": &schema.Schema{ + Type: schema.TypeBool, + Computed: true, + Description: "True if the duration of this lease can be extended through renewal.", + }, + }, + } +} + +func genericSecretDataSourceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Get("path").(string) + + log.Printf("[DEBUG] Reading %s from Vault", path) + secret, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading from Vault: %s", err) + } + + d.SetId(secret.RequestID) + + // Ignoring error because this value came from JSON in the + // first place so no reason why it should fail to re-encode. + jsonDataBytes, _ := json.Marshal(secret.Data) + d.Set("data_json", string(jsonDataBytes)) + + // Since our "data" map can only contain string values, we + // will take strings from Data and write them in as-is, + // and write everything else in as a JSON serialization of + // whatever value we get so that complex types can be + // passed around and processed elsewhere if desired. + dataMap := map[string]string{} + for k, v := range secret.Data { + if vs, ok := v.(string); ok { + dataMap[k] = vs + } else { + // Again ignoring error because we know this value + // came from JSON in the first place and so must be valid. + vBytes, _ := json.Marshal(v) + dataMap[k] = string(vBytes) + } + } + d.Set("data", dataMap) + + d.Set("lease_id", secret.LeaseID) + d.Set("lease_duration", secret.LeaseDuration) + d.Set("lease_start_time", time.Now().Format("RFC3339")) + d.Set("lease_renewable", secret.Renewable) + + return nil +} diff --git a/builtin/providers/vault/data_source_generic_secret_test.go b/builtin/providers/vault/data_source_generic_secret_test.go new file mode 100644 index 000000000..00a5fbb17 --- /dev/null +++ b/builtin/providers/vault/data_source_generic_secret_test.go @@ -0,0 +1,62 @@ +package vault + +import ( + "fmt" + "testing" + + r "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDataSourceGenericSecret(t *testing.T) { + r.Test(t, r.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []r.TestStep{ + r.TestStep{ + Config: testDataSourceGenericSecret_config, + Check: testDataSourceGenericSecret_check, + }, + }, + }) +} + +var testDataSourceGenericSecret_config = ` + +resource "vault_generic_secret" "test" { + path = "secret/foo" + data_json = < Date: Fri, 30 Sep 2016 18:27:05 -0700 Subject: [PATCH 5/5] website: documentation for the vault provider --- website/source/assets/stylesheets/_docs.scss | 1 + .../providers/vault/d/generic_secret.html.md | 81 ++++++++++ .../docs/providers/vault/index.html.markdown | 139 ++++++++++++++++++ .../providers/vault/r/generic_secret.html.md | 69 +++++++++ website/source/layouts/docs.erb | 4 + website/source/layouts/vault.erb | 40 +++++ 6 files changed, 334 insertions(+) create mode 100644 website/source/docs/providers/vault/d/generic_secret.html.md create mode 100644 website/source/docs/providers/vault/index.html.markdown create mode 100644 website/source/docs/providers/vault/r/generic_secret.html.md create mode 100644 website/source/layouts/vault.erb diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index af6776ad4..f01dad600 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -50,6 +50,7 @@ body.layout-template, body.layout-tls, body.layout-ultradns, body.layout-triton, +body.layout-vault, body.layout-vcd, body.layout-vsphere, body.layout-docs, diff --git a/website/source/docs/providers/vault/d/generic_secret.html.md b/website/source/docs/providers/vault/d/generic_secret.html.md new file mode 100644 index 000000000..59ed9b2fe --- /dev/null +++ b/website/source/docs/providers/vault/d/generic_secret.html.md @@ -0,0 +1,81 @@ +--- +layout: "vault" +page_title: "Vault: vault_generic_secret data source" +sidebar_current: "docs-vault-datasource-generic-secret" +description: |- + Reads arbitrary data from a given path in Vault +--- + +# vault\_generic\_secret + +Reads arbitrary data from a given path in Vault. + +This resource is primarily intended to be used with +[Vault's "generic" secret backend](https://www.vaultproject.io/docs/secrets/generic/index.html), +but it is also compatible with any other Vault endpoint that supports +the `vault read` command. + +~> **Important** All data retrieved from Vault will be +written in cleartext to state file generated by Terraform, will appear in +the console output when Terraform runs, and may be included in plan files +if secrets are interpolated into any resource attributes. +Protect these artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +``` +data "vault_generic_secret" "rundeck_auth" { + path = "secret/rundeck_auth" +} + +# Rundeck Provider, for example +provider "rundeck" { + url = "http://rundeck.example.com/" + auth_token = "${data.vault_generic_secret.rundeck_auth.data["auth_token"]}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `path` - (Required) The full logical path from which to request data. +To read data from the "generic" secret backend mounted in Vault by +default, this should be prefixed with `secret/`. Reading from other backends +with this data source is possible; consult each backend's documentation +to see which endpoints support the `GET` method. + +## Required Vault Capabilities + +Use of this resource requires the `read` capability on the given path. + +## Attributes Reference + +The following attributes are exported: + +* `data_json` - A string containing the full data payload retrieved from +Vault, serialized in JSON format. + +* `data` - A mapping whose keys are the top-level data keys returned from +Vault and whose values are the corresponding values. This map can only +represent string data, so any non-string values returned from Vault are +serialized as JSON. + +* `lease_id` - The lease identifier assigned by Vault, if any. + +* `lease_duration` - The duration of the secret lease, in seconds relative +to the time the data was requested. Once this time has passed any plan +generated with this data may fail to apply. + +* `lease_start_time` - As a convenience, this records the current time +on the computer where Terraform is running when the data is requested. +This can be used to approximate the absolute time represented by +`lease_duration`, though users must allow for any clock drift and response +latency relative to to the Vault server. + +* `lease_renewable` - `true` if the lease can be renewed using Vault's +`sys/renew/{lease-id}` endpoint. Terraform does not currently support lease +renewal, and so it will request a new lease each time this data source is +refreshed. diff --git a/website/source/docs/providers/vault/index.html.markdown b/website/source/docs/providers/vault/index.html.markdown new file mode 100644 index 000000000..ab523dab5 --- /dev/null +++ b/website/source/docs/providers/vault/index.html.markdown @@ -0,0 +1,139 @@ +--- +layout: "vault" +page_title: "Provider: Vault" +sidebar_current: "docs-vault-index" +description: |- + The Vault provider allows Terraform to read from, write to, and configure Hashicorp Vault +--- + +# Vault Provider + +The Vault provider allows Terraform to read from, write to, and configure +[Hashicorp Vault](https://vaultproject.io/). + +~> **Important** Interacting with Vault from Terraform causes an secrets +that you read and write to be persisted in both Terraform's state file +*and* in any generated plan files. For any Terraform module that reads or +writes Vault secrets, these files should be treated as sensitive and +protected accordingly. + +This provider serves two pretty-distinct use-cases, which each have their +own security trade-offs and caveats that are covered in the sections that +follow. Consider these carefully before using this provider within your +Terraform configuration. + +## Configuring and Populating Vault + +Terraform can be used by the Vault adminstrators to configure Vault and +populate it with secrets. In this case, the state and any plans associated +with the configuration must be stored and communicated with care, since they +will contain in cleartext any values that were written into Vault. + +Currently Terraform has no mechanism to redact or protect secrets +that are provided via configuration, so teams choosing to use Terraform +for populating Vault secrets should pay careful attention to the notes +on each resource's documentation page about how any secrets are persisted +to the state and consider carefully whether such usage is compatible with +their security policies. + +Except as otherwise noted, the resources that write secrets into Vault are +designed such that they require only the *create* and *update* capabilities +on the relevant resources, so that distinct tokens can be used for reading +vs. writing and thus limit the exposure of a compromised token. + +## Using Vault credentials in Terraform configuration + +Most Terraform providers require credentials to interact with a third-party +service that they wrap. This provider allows such credentials to be obtained +from Vault, which means that operators or systems running Terraform need +only access to a suitably-privileged Vault token in order to temporarily +lease the credentials for other providers. + +Currently Terraform has no mechanism to redact or protect secrets that +are returned via data sources, so secrets read via this provider will be +persisted into the Terraform state, into any plan files, and in some cases +in the console output produced while planning and applying. These artifacts +must therefore all be protected accordingly. + +To reduce the exposure of such secrets, the provider requests a Vault token +with a relatively-short TTL (20 minutes, by default) which in turn means +that where possible Vault will revoke any issued credentials after that +time, but in particular it is unable to retract any static secrets such as +those stored in Vault's "generic" secret backend. + +The requested token TTL can be controlled by the `max_lease_ttl_seconds` +provider argument described below. It is important to consider that Terraform +reads from data sources during the `plan` phase and writes the result into +the plan. Thus a subsequent `apply` will likely fail if it is run after the +intermediate token has expired, due to the revocation of the secrets that +are stored in the plan. + +Except as otherwise noted, the resources that read secrets from Vault +are designed such that they require only the *read* capability on the relevant +resources. + +## Provider Arguments + +The provider configuration block accepts the following arguments. +In most cases it is recommended to set them via the indicated environment +variables in order to keep credential information out of the configuration. + +* `address` - (Required) Origin URL of the Vault server. This is a URL + with a scheme, a hostname and a port but with no path. May be set + via the `VAULT_ADDR` environment variable. + +* `token` - (Required) Vault token that will be used by Terraform to + authenticate. May be set via the `VAULT_TOKEN` environment variable. + Terraform will issue itself a new token that is a child of the one given, + with a short TTL to limit the exposure of any requested secrets. + +* `ca_cert_file` - (Optional) Path to a file on local disk that will be + used to validate the certificate presented by the Vault server. + May be set via the `VAULT_CACERT` environment variable. + +* `ca_cert_dir` - (Optional) Path to a directory on local disk that + contains one or more certificate files that will be used to validate + the certificate presented by the Vault server. May be set via the + `VAULT_CAPATH` environment variable. + +* `client_auth` - (Optional) A configuration block, described below, that + provides credentials used by Terraform to authenticate with the Vault + server. At present there is little reason to set this, because Terraform + does not support the TLS certificate authentication mechanism. + +* `skip_tls_verify` - (Optional) Set this to `true` to disable verification + of the Vault server's TLS certificate. This is strongly discouraged except + in prototype or development environments, since it exposes the possibility + that Terraform can be tricked into writing secrets to a server controlled + by an intruder. May be set via the `VAULT_SKIP_VERIFY` environment variable. + +* `max_lease_ttl_seconds` - (Optional) Used as the duration for the + intermediate Vault token Terraform issues itself, which in turn limits + the duration of secret leases issued by Vault. Defaults to 20 minutes + and may be set via the `TERRAFORM_VAULT_MAX_TTL` environment variable. + See the section above on *Using Vault credentials in Terraform configuration* + for the implications of this setting. + +The `client_auth` configuration block accepts the following arguments: + +* `cert_file` - (Required) Path to a file on local disk that contains the + PEM-encoded certificate to present to the server. + +* `key_file` - (Required) Path to a file on local disk that contains the + PEM-encoded private key for which the authentication certificate was issued. + +## Example Usage + +``` +provider "vault" { + # It is strongly recommended to configure this provider through the + # environment variables described below, so that each user can have + # separate credentials set in the environment. + address = "https://vault.example.net:8200" +} + +data "vault_generic_secret" "example" { + path = "secret/foo" +} +``` + diff --git a/website/source/docs/providers/vault/r/generic_secret.html.md b/website/source/docs/providers/vault/r/generic_secret.html.md new file mode 100644 index 000000000..39c1030ba --- /dev/null +++ b/website/source/docs/providers/vault/r/generic_secret.html.md @@ -0,0 +1,69 @@ +--- +layout: "vault" +page_title: "Vault: vault_generic_secret resource" +sidebar_current: "docs-vault-resource-generic-secret" +description: |- + Writes arbitrary data to a given path in Vault +--- + +# vault\_generic\_secret + +Writes and manages arbitrary data at a given path in Vault. + +This resource is primarily intended to be used with +[Vault's "generic" secret backend](https://www.vaultproject.io/docs/secrets/generic/index.html), +but it is also compatible with any other Vault endpoint that supports +the `vault write` command to create and the `vault delete` command to +delete. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +``` +resource "vault_generic_secret" "example" { + path = "secret/foo" + + data_json = <UltraDNS + > + Vault + + > VMware vCloud Director diff --git a/website/source/layouts/vault.erb b/website/source/layouts/vault.erb new file mode 100644 index 000000000..1d7173fff --- /dev/null +++ b/website/source/layouts/vault.erb @@ -0,0 +1,40 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %>