Enhance SSH backend documentation; remove getting of stored keys and have TTLs honor backends systemview values

This commit is contained in:
Jeff Mitchell
2015-09-21 16:12:38 -04:00
parent 08a81a3364
commit fa53293b7b
9 changed files with 318 additions and 518 deletions

View File

@@ -23,7 +23,7 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ salt, err := salt.NewSalt(conf.StorageView, &salt.Config{
HashFunc: salt.SHA1Hash, HashFunc: salt.SHA256Hash,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -45,7 +45,6 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
}, },
Paths: []*framework.Path{ Paths: []*framework.Path{
pathConfigLease(&b),
pathConfigZeroAddress(&b), pathConfigZeroAddress(&b),
pathKeys(&b), pathKeys(&b),
pathRoles(&b), pathRoles(&b),
@@ -63,30 +62,18 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
} }
const backendHelp = ` const backendHelp = `
The SSH backend generates credentials to establish SSH connection with remote hosts. The SSH backend generates credentials allowing clients to establish SSH
There are two types of credentials that could be generated: Dynamic and OTP. The connections to remote hosts.
desired way of key creation should be chosen by using 'key_type' parameter of 'roles/'
endpoint. When a credential is requested for a particular role, Vault will generate
a credential accordingly and issue it.
Dynamic Key: is a RSA private key which can be used to establish SSH session using There are two variants of the backend, which generate different types of
publickey authentication. When the client receives a key and uses it to establish credentials: dynamic keys and One-Time Passwords (OTPs). The desired behavior
connections with hosts, Vault server will have no way to know when and how many is role-specific and chosen at role creation time with the 'key_type'
times the key will be used. So, these login attempts will not be audited by Vault. parameter.
To create a dynamic credential, Vault will use the shared private key registered
with the role. Named key should be created using 'keys/' endpoint and used with
'roles/' endpoint for Vault to know the shared key to use for installing the newly
generated key. Since Vault uses the shared key to install keys for other usernames,
shared key should have sudoer privileges in remote hosts and password prompts for
sudoers should be disabled. Also, dynamic keys are leased keys and gets revoked
in remote hosts by Vault after the expiry.
OTP Key: is a UUID which can be used to login using keyboard-interactive authentication. Please see the backend documentation for a thorough description of both
All the hosts that intend to support OTP should have Vault SSH Agent installed in types. The Vault team strongly recommends the OTP type.
them. This agent will receive the OTP from client and get it validated by Vault server.
And since Vault server has a role to play for each successful connection, all the
events will be audited. Vault server validates a key only once, hence it is a OTP.
After mounting this backend, before generating the keys, configure the lease using After mounting this backend, before generating credentials, configure the
'congig/lease' endpoint and create roles using 'roles/' endpoint. backend's lease behavior using the 'config/lease' endpoint and create roles
using the 'roles/' endpoint.
` `

View File

@@ -6,6 +6,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@@ -54,6 +55,19 @@ oOyBJU/HMVvBfv4g+OVFLVgSwwm6owwsouZ0+D/LasbuHqYyqYqdyPJQYzWA2Y+F
` `
) )
func testingFactory(conf *logical.BackendConfig) (logical.Backend, error) {
defaultLeaseTTLVal := 2 * time.Minute
maxLeaseTTLVal := 10 * time.Minute
return Factory(&logical.BackendConfig{
Logger: nil,
StorageView: &logical.InmemStorage{},
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: defaultLeaseTTLVal,
MaxLeaseTTLVal: maxLeaseTTLVal,
},
})
}
var testIP string var testIP string
var testUserName string var testUserName string
@@ -101,7 +115,7 @@ func TestSSHBackend_Lookup(t *testing.T) {
resp3 := []string{testDynamicRoleName, testOTPRoleName} resp3 := []string{testDynamicRoleName, testOTPRoleName}
resp4 := []string{testDynamicRoleName} resp4 := []string{testDynamicRoleName}
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testLookupRead(t, data, resp1), testLookupRead(t, data, resp1),
testRoleWrite(t, testOTPRoleName, testOTPRoleData), testRoleWrite(t, testOTPRoleName, testOTPRoleData),
@@ -123,7 +137,7 @@ func TestSSHBackend_DynamicKeyCreate(t *testing.T) {
"ip": testIP, "ip": testIP,
} }
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey), testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData), testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
@@ -140,7 +154,7 @@ func TestSSHBackend_OTPRoleCrud(t *testing.T) {
"cidr_list": testCIDRList, "cidr_list": testCIDRList,
} }
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testRoleWrite(t, testOTPRoleName, testOTPRoleData), testRoleWrite(t, testOTPRoleName, testOTPRoleData),
testRoleRead(t, testOTPRoleName, respOTPRoleData), testRoleRead(t, testOTPRoleName, respOTPRoleData),
@@ -162,7 +176,7 @@ func TestSSHBackend_DynamicRoleCrud(t *testing.T) {
"key_type": testDynamicKeyType, "key_type": testDynamicKeyType,
} }
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey), testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData), testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
@@ -175,11 +189,9 @@ func TestSSHBackend_DynamicRoleCrud(t *testing.T) {
func TestSSHBackend_NamedKeysCrud(t *testing.T) { func TestSSHBackend_NamedKeysCrud(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testNamedKeysRead(t, ""),
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey), testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testNamedKeysRead(t, testSharedPrivateKey),
testNamedKeysDelete(t), testNamedKeysDelete(t),
}, },
}) })
@@ -191,7 +203,7 @@ func TestSSHBackend_OTPCreate(t *testing.T) {
"ip": testIP, "ip": testIP,
} }
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testRoleWrite(t, testOTPRoleName, testOTPRoleData), testRoleWrite(t, testOTPRoleName, testOTPRoleData),
testCredsWrite(t, testOTPRoleName, data, false), testCredsWrite(t, testOTPRoleName, data, false),
@@ -207,7 +219,7 @@ func TestSSHBackend_VerifyEcho(t *testing.T) {
"message": api.VerifyEchoResponse, "message": api.VerifyEchoResponse,
} }
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testVerifyWrite(t, verifyData, expectedData), testVerifyWrite(t, verifyData, expectedData),
}, },
@@ -232,7 +244,7 @@ func TestSSHBackend_ConfigZeroAddressCRUD(t *testing.T) {
} }
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testRoleWrite(t, testOTPRoleName, testOTPRoleData), testRoleWrite(t, testOTPRoleName, testOTPRoleData),
testConfigZeroAddressWrite(t, req1), testConfigZeroAddressWrite(t, req1),
@@ -272,7 +284,7 @@ func TestSSHBackend_CredsForZeroAddressRoles(t *testing.T) {
"roles": fmt.Sprintf("%s,%s", testOTPRoleName, testDynamicRoleName), "roles": fmt.Sprintf("%s,%s", testOTPRoleName, testDynamicRoleName),
} }
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory, Factory: testingFactory,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testRoleWrite(t, testOTPRoleName, otpRoleData), testRoleWrite(t, testOTPRoleName, otpRoleData),
testCredsWrite(t, testOTPRoleName, data, true), testCredsWrite(t, testOTPRoleName, data, true),
@@ -352,31 +364,6 @@ func testVerifyWrite(t *testing.T, data map[string]interface{}, expected map[str
} }
} }
func testNamedKeysRead(t *testing.T, key string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: fmt.Sprintf("keys/%s", testKeyName),
Check: func(resp *logical.Response) error {
if key != "" {
if resp == nil || resp.Data == nil {
return fmt.Errorf("Key missing in response")
}
var d struct {
Key string `mapstructure:"key"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
if d.Key != key {
return fmt.Errorf("Key mismatch")
}
}
return nil
},
}
}
func testNamedKeysWrite(t *testing.T, name, key string) logicaltest.TestStep { func testNamedKeysWrite(t *testing.T, name, key string) logicaltest.TestStep {
return logicaltest.TestStep{ return logicaltest.TestStep{
Operation: logical.WriteOperation, Operation: logical.WriteOperation,

View File

@@ -1,108 +0,0 @@
package ssh
import (
"fmt"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
type configLease struct {
Lease time.Duration
LeaseMax time.Duration
}
func pathConfigLease(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/lease",
Fields: map[string]*framework.FieldSchema{
"lease": &framework.FieldSchema{
Type: framework.TypeString,
Description: "[Required] Default lease for roles.",
},
"lease_max": &framework.FieldSchema{
Type: framework.TypeString,
Description: "[Required] Maximum time a credential is valid for.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathConfigLeaseWrite,
},
HelpSynopsis: pathConfigLeaseHelpSyn,
HelpDescription: pathConfigLeaseHelpDesc,
}
}
func (b *backend) pathConfigLeaseWrite(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
leaseRaw := d.Get("lease").(string)
if leaseRaw == "" {
return logical.ErrorResponse("Missing lease"), nil
}
leaseMaxRaw := d.Get("lease_max").(string)
if leaseMaxRaw == "" {
return logical.ErrorResponse("Missing lease_max"), nil
}
lease, err := time.ParseDuration(leaseRaw)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid 'lease': %s", err)), nil
}
leaseMax, err := time.ParseDuration(leaseMaxRaw)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid 'lease_max': %s", err)), nil
}
entry, err := logical.StorageEntryJSON("config/lease", &configLease{
Lease: lease,
LeaseMax: leaseMax,
})
if err != nil {
return nil, fmt.Errorf("could not create storage entry JSON: %s", err)
}
if err := req.Storage.Put(entry); err != nil {
return nil, fmt.Errorf("could not store JSON: %s", err)
}
return nil, nil
}
func (b *backend) Lease(s logical.Storage) (*configLease, error) {
entry, err := s.Get("config/lease")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result configLease
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
const pathConfigLeaseHelpSyn = `
Configure the default lease information for SSH dynamic keys.
`
const pathConfigLeaseHelpDesc = `
This configures the default lease information used for SSH keys generated by
this backend. The lease specifies the duration that a credential will be valid
for, as well as the maximum session for a set of credentials.
The format for the lease is "1h" or integer and then unit. The longest
unit is hour.
`

View File

@@ -157,20 +157,8 @@ func (b *backend) pathCredsCreateWrite(
return nil, fmt.Errorf("key type unknown") return nil, fmt.Errorf("key type unknown")
} }
// Change the lease information to reflect user's choice result.Secret.TTL = b.System().DefaultLeaseTTL()
lease, _ := b.Lease(req.Storage)
// If the lease information is set, update it in secret.
if lease != nil {
result.Secret.TTL = lease.Lease
result.Secret.GracePeriod = lease.LeaseMax
}
// If lease information is not set, set it to 10 minutes.
if lease == nil {
result.Secret.TTL = 10 * time.Minute
result.Secret.GracePeriod = 2 * time.Minute result.Secret.GracePeriod = 2 * time.Minute
}
return result, nil return result, nil
} }

View File

@@ -27,7 +27,6 @@ func pathKeys(b *backend) *framework.Path {
}, },
}, },
Callbacks: map[logical.Operation]framework.OperationFunc{ Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathKeysRead,
logical.WriteOperation: b.pathKeysWrite, logical.WriteOperation: b.pathKeysWrite,
logical.DeleteOperation: b.pathKeysDelete, logical.DeleteOperation: b.pathKeysDelete,
}, },
@@ -52,22 +51,6 @@ func (b *backend) getKey(s logical.Storage, n string) (*sshHostKey, error) {
return &result, nil return &result, nil
} }
func (b *backend) pathKeysRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
key, err := b.getKey(req.Storage, d.Get("key_name").(string))
if err != nil {
return nil, err
}
if key == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"key": key.Key,
},
}, nil
}
func (b *backend) pathKeysDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { func (b *backend) pathKeysDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
keyName := d.Get("key_name").(string) keyName := d.Get("key_name").(string)
keyPath := fmt.Sprintf("keys/%s", keyName) keyPath := fmt.Sprintf("keys/%s", keyName)
@@ -120,7 +103,7 @@ Vault uses this key to install and uninstall dynamic keys in remote hosts. This
key should have sudoer privileges in remote hosts. This enables installing keys key should have sudoer privileges in remote hosts. This enables installing keys
for unprivileged usernames. for unprivileged usernames.
If this backend is mounted as "ssh", then the endpoint for registering shared key If this backend is mounted as "ssh", then the endpoint for registering shared
is "ssh/keys/webrack", if "webrack" is the user coined name for the key. The name key is "ssh/keys/<name>". The name given here can be associated with any number
given here can be associated with any number of roles via the endpoint "ssh/roles/". of roles via the endpoint "ssh/roles/".
` `

View File

@@ -23,7 +23,7 @@ func secretDynamicKey(b *backend) *framework.Secret {
Description: "IP address of host", Description: "IP address of host",
}, },
}, },
DefaultDuration: 10 * time.Minute, DefaultDuration: 0, // this will use sysview's value
DefaultGracePeriod: 2 * time.Minute, DefaultGracePeriod: 2 * time.Minute,
Renew: b.secretDynamicKeyRenew, Renew: b.secretDynamicKeyRenew,
Revoke: b.secretDynamicKeyRevoke, Revoke: b.secretDynamicKeyRevoke,
@@ -31,14 +31,7 @@ func secretDynamicKey(b *backend) *framework.Secret {
} }
func (b *backend) secretDynamicKeyRenew(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { func (b *backend) secretDynamicKeyRenew(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
lease, err := b.Lease(req.Storage) f := framework.LeaseExtend(b.System().DefaultLeaseTTL(), b.System().MaxLeaseTTL(), false)
if err != nil {
return nil, err
}
if lease == nil {
lease = &configLease{Lease: 1 * time.Hour}
}
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax, false)
return f(req, d) return f(req, d)
} }

View File

@@ -19,7 +19,7 @@ func secretOTP(b *backend) *framework.Secret {
Description: "One time password", Description: "One time password",
}, },
}, },
DefaultDuration: 10 * time.Minute, DefaultDuration: 0, // this will use sysview's value
DefaultGracePeriod: 2 * time.Minute, DefaultGracePeriod: 2 * time.Minute,
Revoke: b.secretOTPRevoke, Revoke: b.secretOTPRevoke,
} }

View File

@@ -66,6 +66,33 @@ employees actively contribute to Vault.
</div> </div>
</div> </div>
<div class="person">
<img class="pull-left" src="https://s.gravatar.com/avatar/b32bf8e58ce1929d2d676a353c88f539.png?s=125">
<div class="bio">
<h3>Jeff Mitchell (<a href="https://github.com/jefferai">@jefferai</a>)</h3>
<p>
Jeff Mitchell is a core contributor to Vault. He works on all layers of
Vault, from the core to backends. Jeff is an employee of HashiCorp and
has also contributed to
<a href="https://www.consul.io">Consul</a> and
<a href="https://www.terraform.io">Terraform</a>,
as well as many other open-source projects.
</p>
</div>
</div>
<div class="person">
<img class="pull-left" src="https://s.gravatar.com/avatar/824b1038ad73d555cf26ef6096bd46ce.png?s=125">
<div class="bio">
<h3>Vishal Nayak (<a href="https://github.com/vishalnayak">@vishalnayak</a>)</h3>
<p>
Vishal Nayak is a contributor to Vault. He works on all layers of Vault,
from the core to backends. Vishal is currently finishing his Master's
degree, after which he will be joining full-time.
</p>
</div>
</div>
<div class="person"> <div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/2acc31dd6370a54b18f6755cd0710ce6.png?s=125"> <img class="pull-left" src="http://www.gravatar.com/avatar/2acc31dd6370a54b18f6755cd0710ce6.png?s=125">
<div class="bio"> <div class="bio">

View File

@@ -10,121 +10,207 @@ description: |-
Name: `ssh` Name: `ssh`
Vault SSH backend generates SSH credentials for remote hosts dynamically. This Vault SSH backend dynamically generates SSH credentials for remote hosts. This
backend increases the security by removing the need to share the private key to increases security by removing the need to share private keys with all users
everyone who needs access to infrastructures. It also solves the problem of needing access to infrastructure. It also solves the problem of management and distribution of keys belonging to remote hosts.
management and distribution of keys belonging to remote hosts.
This backend supports two types of credential creation: Dynamic and OTP. Both of This backend supports two types of credential creation: Dynamic Key and
them addresses the problems in different ways. One-Time Password (OTP), which address these problems in different ways.
Read and carefully understand both of them and choose the one which best suits Read and carefully understand both of them before choosing the one which best
your needs. suits your needs. The Vault team strongly recommends the OTP type whenever
possible, and the drawbacks to the dynamic key type should be carefully considered
before choosing it.
This page will show a quick start for this backend. For detailed documentation This page will show a quick start for this backend. For detailed documentation
on every path, use `vault path-help` after mounting the backend. on every path, use `vault path-help` after mounting the backend.
----------------------------------------------------
## I. Dynamic Type
Register the shared secret key (having super user privileges) with Vault and let
Vault take care of issuing a dynamic secret key every time a client wants to SSH
into the remote host.
When a Vault authenticated client requests for a dynamic credential, Vault server
creates a key-pair, uses the previously shared secret key to login to the remote
host and appends the newly generated public key to `~/.ssh/authorized_keys` file for
the desired username. Vault uses an install script (configurable) to achieve this.
To run this script in super user mode without password prompts, `NOPASSWD` option
for sudoers should be enabled at all remote hosts.
File: `/etc/sudoers`
```hcl
%sudo ALL=(ALL)NOPASSWD: ALL
```
The private key returned to the user will be leased and can be renewed if desired.
Once the key is given to the user, Vault will not know when it gets used or how many
time it gets used. Therefore, Vault **WILL NOT** and cannot audit the SSH session
establishments. An alternative is to use OTP type, which audits every SSH request
(see below).
### Mounting SSH ### Mounting SSH
`ssh` backend is not mounted by default. So, the first step in using the SSH backend The `ssh` backend is not mounted by default and needs to be explicitly mounted.
is to mount it. This is a common step for both OTP and Dynamic Key types.
```shell ```shell
$ vault mount ssh $ vault mount ssh
Successfully mounted 'ssh' at 'ssh'! Successfully mounted 'ssh' at 'ssh'!
``` ```
Next, we must register infrastructures with Vault. This is done by writing the role ----------------------------------------------------
information. The type of credentials created are determined by the `key_type` option. ## I. One-Time-Password (OTP) Type
To do this, first create a named key and then create a role.
### Registering shared secret key This backend type allows a Vault server to issue an OTP every time a client
wants to SSH into a remote host, using a helper command on the remote host to
perform verification through a helper command.
Create a named key, say `dev_key`, which represents a registered shared private key. An authenticated client requests credentials from the Vault server and, if
Remember that this key should be of admin user with super user privileges. authorized, is issued an OTP. When the client establishes an SSH connection
to the desired remote host, the OTP used during SSH authentication is received
by the Vault helper, which then validates the OTP with the Vault server. The
Vault server then deletes this OTP, ensuring that it is only used once.
Since the Vault server is contacted during SSH connection establishment, every
login attempt and the correlating Vault lease information is logged to the
audit backend.
See [Vault-SSH-Helper](https://github.com/hashicorp/vault-ssh-helper) for
details on the helper.
### Drawbacks
The main concern with the OTP backend type is the remote host's connection to
Vault; if compromised, an attacker could spoof the Vault server returning
a successful request. This risk can be mitigated by using TLS for the
connection to Vault and checking certificate validity; future enhancements to
this backend may address this problem in a more concrete way.
### Creating a Role
Create a role with the `key_type` parameter set to `otp`. All of the machines
represented by the role's CIDR list should have helper properly installed and
configured.
```shell
$ vault write ssh/roles/otp_key_role key_type=otp default_user=username cidr_list=x.x.x.x/y,m.m.m.m/n
Success! Data written to: ssh/roles/otp_key_role
```
### Create a Credential
Create an OTP credential for an IP that belongs to `otp_key_role`.
```shell
$ vault write ssh/creds/otp_key_role ip=x.x.x.x
Key Value
lease_id ssh/creds/otp_key_role/73bbf513-9606-4bec-816c-5a2f009765a5
lease_duration 600
lease_renewable false
port 22
username username
ip x.x.x.x
key 2f7e25a2-24c9-4b7b-0d35-27d5e5203a5c
key_type otp
```
### Establish an SSH session
```shell
$ ssh username@localhost
Password: <Enter OTP>
username@ip:~$
```
### Automate it!
A single CLI command can be used to create a new OTP and invoke SSH with the
correct paramters to connect to the host.
```shell
$ vault ssh -role otp_key_role username@x.x.x.x
OTP for the session is `b4d47e1b-4879-5f4e-ce5c-7988d7986f37`
[Note: Install `sshpass` to automate typing in OTP]
Password: <Enter OTP>
```
The OTP will be entered automatically using `sshpass` if it is installed.
```shell
$ vault ssh -role otp_key_role username@x.x.x.x
username@ip:~$
```
----------------------------------------------------
## II. Dynamic Key Type
When using this type, the administrator registers a secret key with appropriate
`sudo` privileges on the remote machines; for every authorized credential
request, Vault creates a new SSH key pair and appends the newly-generated
public key to the `authorized_keys` file for the configured username on the
remote host. Vault uses a configurable install script to achieve this.
The backend does not prompt for `sudo` passwords; the `NOPASSWD` option
for sudoers should be enabled at all remote hosts for the Vault administrative
user.
The private key returned to the user will be leased and can be renewed if
desired. Once the key is given to the user, Vault will not know when it gets
used or how many time it gets used. Therefore, Vault **WILL NOT** and cannot
audit the SSH session establishments.
When the credential lease expires, Vault removes the secret key from the remote
machine.
### Drawbacks
The dynamic key type has several serious drawbacks:
1. _Audit logs are unreliable_: Vault can only log when users request
credentials, not when they use the given keys. If user A and user B both
request access to a machine, and are given a lease valid for five minutes,
it is impossible to know whether two accesses to that user account on the
remote machine were A, A; A, B; B, A; or B, B.
2. _Generating dynamic keys consumes entropy_: Unless equipped with a hardware
entropy generating device, a machine can quickly run out of entropy when
generating SSH keys. This will cause further requests for various Vault
operations to stall until more entropy is available, which could take a
significant amount of time, after which the next request for a new SSH key
will use the generated entropy and cause stalling again.
Because of these drawbacks, the Vault team recommends use of the OTP type
whenever possible. Care should be taken with respect to the above issues with
any deployments using the dynamic key type.
### sudo
In order to adjust the `authorized_keys` file for the desired user, Vault
connects via SSH to the remote machine as a separate user, and uses `sudo` to
gain the privileges required. An example `sudoers` file is shown below.
File: `/etc/sudoers`
```hcl
# This is a sample sudoers statement; you should modify it
# as appropriate to satisfy your security needs.
vaultadmin ALL=(ALL)NOPASSWD: ALL
```
### Configuration
Next, infrastructure configuration must be registered with Vault via roles.
First, however, the shared secret key must be specified.
#### Registering the shared secret key
Register a key with a name; this key must have administrative capabilities
on the remote hosts.
```shell ```shell
$ vault write ssh/keys/dev_key key=@dev_shared_key.pem $ vault write ssh/keys/dev_key key=@dev_shared_key.pem
``` ```
### Create a Role #### Create a Role
Create a role, say `dynamic_key_role`. All the machines represented by CIDR block Next, create a role. All of the machines contained within this CIDR block list
should be accessible through `dev_key` with root privileges. should be accessible using the registered shared secret key.
```shell ```shell
$ vault write ssh/roles/dynamic_key_role key_type=dynamic key=dev_key admin_user=username default_user=username cidr_list=x.x.x.x/y $ vault write ssh/roles/dynamic_key_role key_type=dynamic key=dev_key admin_user=username default_user=username cidr_list=x.x.x.x/y
Success! Data written to: ssh/roles/dynamic_key_role Success! Data written to: ssh/roles/dynamic_key_role
``` ```
Option `cidr_list` is optional and defaults to zero-address (0.0.0.0/0). `cidr_list` is optional and defaults to the zero address (0.0.0.0/0), e.g. all
hosts.
Use the `install_script` option to provide an install script if hosts does not Use the `install_script` option to provide an install script if the remote
resemble typical Linux machine. The default script is compiled into the binary. hosts do not resemble a typical Linux machine. The default script is compiled
It is straight forward and is shown below. The script takes three arguments which into the Vault binary, but it is straight forward to specify an alternate.
are explained in the comments. The script takes three arguments which are explained in the comments.
```shell To see the default, see [linux_install_script.go](https://github.com/hashicorp/vault/blob/master/builtin/logical/ssh/linux_install_script.go)
# This script file installs or uninstalls an RSA public key to/from authoried_keys
# file in a typical linux machine. This script should be registered with vault
# server while creating a role for key type 'dynamic'.
# $1: "install" or "uninstall"
#
# $2: File name containing public key to be installed. Vault server uses UUID
# as file name to avoid collisions with public keys generated for requests.
#
# $3: Absolute path of the authorized_keys file.
if [ $1 != "install" && $1 != "uninstall" ]; then
exit 1
fi
# If the key being installed is already present in the authorized_keys file, it is
# removed and the result is stored in a temporary file.
grep -vFf $2 $3 > temp_$2
# Contents of temporary file will be the contents of authorized_keys file.
cat temp_$2 | sudo tee $3
if [ $1 == "install" ]; then
# New public key is appended to authorized_keys file
cat $2 | sudo tee --append $3
fi
# Auxiliary files are deleted
rm -f $2 temp_$2
```
### Create a credential ### Create a credential
Create a dynamic key for an IP that belongs to `dynamic_key_role`. Create a dynamic key for an IP that is covered by `dynamic_key_role`'s CIDR
list.
```shell ```shell
$ vault write ssh/creds/dynamic_key_role ip=x.x.x.x $ vault write ssh/creds/dynamic_key_role ip=x.x.x.x
@@ -167,7 +253,8 @@ username username
### Establish an SSH session ### Establish an SSH session
Save the key to a file, say `dyn_key.pem`, and then use it to establish an SSH session. Save the key to a file (e.g. `dyn_key.pem`) and then use it to establish an
SSH session.
```shell ```shell
$ ssh -i dyn_key.pem username@ip $ ssh -i dyn_key.pem username@ip
@@ -176,139 +263,17 @@ username@ip:~$
### Automate it! ### Automate it!
Creation of new key, saving it in a file and establishing an SSH session will all be done Creation of new key, saving to a file, and using it to establish an SSH session
via a single Vault CLI. can all be done with a single Vault CLI command.
```shell ```shell
$ vault ssh -role dynamic_key_role username@ip $ vault ssh -role dynamic_key_role username@ip
username@ip:~$ username@ip:~$
``` ```
---------------------------------------------------- ----------------------------------------------------
## II. One-Time-Password (OTP) Type
Install Vault SSH Agent in remote hosts and let Vault server issue an OTP every time
a client wants to SSH into remote hosts.
Vault authenticated clients request for a credential from Vault server and get an OTP
issued. When clients try to establish SSH connection with the remote host, OTP typed
in at the password prompt will be received by the Vault agent and gets validated
by the Vault server. Vault server deletes the OTP after validating it once (hence one-time).
Since Vault server is contacted for every successful connection establishment, unlike
Dynamic type, every login attempt **WILL** be audited.
See [Vault-SSH-Agent](https://github.com/hashicorp/vault-ssh-agent) for details
on how to configure the agent.
### Mounting SSH
`ssh` backend is not mounted by default and needs to be explicitly mounted. This is
a common step for both OTP and Dynamic types.
```shell
$ vault mount ssh
Successfully mounted 'ssh' at 'ssh'!
```
### Creating a Role
Create a role, say `otp_key_role` for key type `otp`. All the machines represented
by CIDR block should have agent installed in them and have their SSH configuration
modified to support Vault SSH Agent client authentication.
```shell
$ vault write ssh/roles/otp_key_role key_type=otp default_user=username cidr_list=x.x.x.x/y,m.m.m.m/n
Success! Data written to: ssh/roles/otp_key_role
```
### Create a Credential
Create an OTP credential for an IP that belongs to `otp_key_role`.
```shell
$ vault write ssh/creds/otp_key_role ip=x.x.x.x
Key Value
lease_id ssh/creds/otp_key_role/73bbf513-9606-4bec-816c-5a2f009765a5
lease_duration 600
lease_renewable false
port 22
username username
ip x.x.x.x
key 2f7e25a2-24c9-4b7b-0d35-27d5e5203a5c
key_type otp
```
### Establish an SSH session
```shell
$ ssh username@localhost
Password: <Enter OTP>
username@ip:~$
```
### Automate it!
Creation of new OTP and running SSH command can be done via a single CLI.
```shell
$ vault ssh -role otp_key_role username@x.x.x.x
OTP for the session is `b4d47e1b-4879-5f4e-ce5c-7988d7986f37`
[Note: Install `sshpass` to automate typing in OTP]
Password: <Enter OTP>
```
OTP will be typed in using `sshpass` if it is installed.
```shell
$ vault ssh -role otp_key_role username@x.x.x.x
username@ip:~$
```
----------------------------------------------------
## API ## API
### /ssh/config/lease
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Configures the lease settings for generated credentials.
This is a root protected endpoint.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/ssh/config/lease`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">lease</span>
<span class="param-flags">required</span>
(String)
The lease value provided as a duration
with time suffix. Hour is the largest suffix.
</li>
<li>
<span class="param">lease_max</span>
<span class="param-flags">required</span>
(String)
The maximum lease value provided as a duration
with time suffix. Hour is the largest suffix.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>
### /ssh/keys/ ### /ssh/keys/
#### POST #### POST
@@ -331,7 +296,7 @@ username@ip:~$
<span class="param">key</span> <span class="param">key</span>
<span class="param-flags">required</span> <span class="param-flags">required</span>
(String) (String)
SSH private key with super user privileges in host SSH private key with appropriate privileges on remote hosts.
</li> </li>
</ul> </ul>
</dd> </dd>
@@ -341,34 +306,6 @@ username@ip:~$
A `204` response code. A `204` response code.
</dd> </dd>
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Queries a named key. This is a root protected endpoint.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/ssh/keys/<key name>`</dd>
<dt>Parameters</dt>
<dd>None</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAvYvoRcWRxqOim5VZnuM6wHCbLUeiND0yaM1tvOl+Fsrz55DG\nA0OZp4RGAu1Fgr46E1mzxFz1+zY4UbcEExg+u21fpa8YH8sytSWW1FyuD8ICib0A\n/l8slmDMw4BkkGOtSlEqgscpkpv/TWZD1NxJWkPcULk8z6c7TOETn2/H9mL+v2RE\nmbE6NDEwJKfD3MvlpIqCP7idR+86rNBAODjGOGgyUbtFLT+K01XmDRALkV3V/nh+\nGltyjL4c6RU4zG2iRyV5RHlJtkml+UzUMkzr4IQnkCC32CC/wmtoo/IsAprpcHVe\nnkBn3eFQ7uND70p5n6GhN/KOh2j519JFHJyokwIDAQABAoIBAHX7VOvBC3kCN9/x\n+aPdup84OE7Z7MvpX6w+WlUhXVugnmsAAVDczhKoUc/WktLLx2huCGhsmKvyVuH+\nMioUiE+vx75gm3qGx5xbtmOfALVMRLopjCnJYf6EaFA0ZeQ+NwowNW7Lu0PHmAU8\nZ3JiX8IwxTz14DU82buDyewO7v+cEr97AnERe3PUcSTDoUXNaoNxjNpEJkKREY6h\n4hAY676RT/GsRcQ8tqe/rnCqPHNd7JGqL+207FK4tJw7daoBjQyijWuB7K5chSal\noPInylM6b13ASXuOAOT/2uSUBWmFVCZPDCmnZxy2SdnJGbsJAMl7Ma3MUlaGvVI+\nTfh1aQkCgYEA4JlNOabTb3z42wz6mz+Nz3JRwbawD+PJXOk5JsSnV7DtPtfgkK9y\n6FTQdhnozGWShAvJvc+C4QAihs9AlHXoaBY5bEU7R/8UK/pSqwzam+MmxmhVDV7G\nIMQPV0FteoXTaJSikhZ88mETTegI2mik+zleBpVxvfdhE5TR+lq8Br0CgYEA2AwJ\nCUD5CYUSj09PluR0HHqamWOrJkKPFPwa+5eiTTCzfBBxImYZh7nXnWuoviXC0sg2\nAuvCW+uZ48ygv/D8gcz3j1JfbErKZJuV+TotK9rRtNIF5Ub7qysP7UjyI7zCssVM\nkuDd9LfRXaB/qGAHNkcDA8NxmHW3gpln4CFdSY8CgYANs4xwfercHEWaJ1qKagAe\nrZyrMpffAEhicJ/Z65lB0jtG4CiE6w8ZeUMWUVJQVcnwYD+4YpZbX4S7sJ0B8Ydy\nAhkSr86D/92dKTIt2STk6aCN7gNyQ1vW198PtaAWH1/cO2UHgHOy3ZUt5X/Uwxl9\ncex4flln+1Viumts2GgsCQKBgCJH7psgSyPekK5auFdKEr5+Gc/jB8I/Z3K9+g4X\n5nH3G1PBTCJYLw7hRzw8W/8oALzvddqKzEFHphiGXK94Lqjt/A4q1OdbCrhiE68D\nMy21P/dAKB1UYRSs9Y8CNyHCjuZM9jSMJ8vv6vG/SOJPsnVDWVAckAbQDvlTHC9t\nO98zAoGAcbW6uFDkrv0XMCpB9Su3KaNXOR0wzag+WIFQRXCcoTvxVi9iYfUReQPi\noOyBJU/HMVvBfv4g+OVFLVgSwwm6owwsouZ0+D/LasbuHqYyqYqdyPJQYzWA2Y+F\n+B6f4RoPdSXj24JHPg/ioRxjaj094UXJxua2yfkcecGNEuBQHSs=\n-----END RSA PRIVATE KEY-----\n"
}
```
</dd>
#### DELETE #### DELETE
<dl class="api"> <dl class="api">
@@ -411,20 +348,22 @@ username@ip:~$
<ul> <ul>
<li> <li>
<span class="param">key</span> <span class="param">key</span>
<span class="param-flags">required for Dynamic type, NA for OTP type</span> <span class="param-flags">required for Dynamic Key type, N/A for
OTP type</span>
(String) (String)
Name of the registered key in Vault. Before creating the role, use Name of the registered key in Vault. Before creating the role, use
the `keys/` endpoint to create a named key. the `keys/` endpoint to create a named key.
</li> </li>
<li> <li>
<span class="param">admin_user</span> <span class="param">admin_user</span>
<span class="param-flags">required for Dynamic type, NA for OTP type</span> <span class="param-flags">required for Dynamic Key type, N/A for OTP
type</span>
(String) (String)
Admin user at remote host. The shared key being registered should be Admin user at remote host. The shared key being registered should
for this user and should have root privileges. Everytime a dynamic be for this user and should have root or sudo privileges. Every
credential is being generated for other users, Vault uses this admin time a dynamic credential is generated for a client,
username to login to remote host and install the generated credential Vault uses this admin username to login to remote host and install
for the other user. the generated credential.
</li> </li>
<li> <li>
<span class="param">default_user</span> <span class="param">default_user</span>
@@ -438,63 +377,66 @@ username@ip:~$
<span class="param">cidr_list</span> <span class="param">cidr_list</span>
<span class="param-flags">optional for both types</span> <span class="param-flags">optional for both types</span>
(String) (String)
Comma separated list of CIDR blocks for which the role is applicable for. Comma separated list of CIDR blocks for which the role is
CIDR blocks can belong to more than one role. Defaults to zero-address (0.0.0.0/0). applicable for. CIDR blocks can belong to more than one role.
Defaults to the zero address (0.0.0.0/0).
</li> </li>
<li> <li>
<span class="param">exclude_cidr_list</span> <span class="param">exclude_cidr_list</span>
<span class="param-flags">optional for both types</span> <span class="param-flags">optional for both types</span>
(String) (String)
Comma separated list of CIDR blocks. IP addresses belonging to these blocks are not Comma-separated list of CIDR blocks. IP addresses belonging to
accepted by the role. This is particularly useful when big CIDR blocks are being used these blocks are not accepted by the role. This is particularly
by the role and certain parts of it needs to be kept out. useful when big CIDR blocks are being used by the role and certain
parts need to be kept out.
</li> </li>
<li> <li>
<span class="param">port</span> <span class="param">port</span>
<span class="param-flags">optional for both types</span> <span class="param-flags">optional for both types</span>
(Integer) (Integer)
Port number for SSH connection. Default is '22'. Port number does not Port number for SSH connection. The default is '22'. Port number
play any role in creation of OTP. For 'otp' type, this is just a way does not play any role in OTP generation. For the 'otp' backend
to inform client about the port number to use. Port number will be type, this is just a way to inform the client about the port number
returned to client by Vault server along with OTP. to use. The port number will be returned to the client by Vault
along with the OTP.
</li> </li>
<li> <li>
<span class="param">key_type</span> <span class="param">key_type</span>
<span class="param-flags">required for both types</span> <span class="param-flags">required for both types</span>
(String) (String)
Type of key used to login to hosts. It can be either `otp` or `dynamic`. Type of credentials generated by this role. Can be either `otp` or
`otp` type requires agent to be installed in remote hosts. `dynamic`.
</li> </li>
<li> <li>
<span class="param">key_bits</span> <span class="param">key_bits</span>
<span class="param-flags">optional for Dynamic type, NA for OTP type</span> <span class="param-flags">optional for Dynamic Key type, N/A for OTP type</span>
(Integer) (Integer)
Length of the RSA dynamic key in bits. It is 1024 by default or it can be 2048. Length of the RSA dynamic key in bits; can be either 1024 or 2048.
1024 the default.
</li> </li>
<li> <li>
<span class="param">install_script</span> <span class="param">install_script</span>
<span class="param-flags">optional for Dynamic type, NA for OTP type</span> <span class="param-flags">optional for Dynamic Key type, N/A for OTP type</span>
(String) (String)
Script used to install and uninstall public keys in the target machine. Script used to install and uninstall public keys in the target
The inbuilt default install script will be for Linux hosts. machine. Defaults to the built-in script.
</li> </li>
<li> <li>
<span class="param">allowed_users</span> <span class="param">allowed_users</span>
<span class="param-flags">optional for both types</span> <span class="param-flags">optional for both types</span>
(String) (String)
If this option is not specified, client can request for a credential for If this option is not specified, a client can request credentials
any valid user at the remote host, including the admin user. If only certain to log into any valid user at the remote host, including the admin
usernames are to be allowed, then this list enforces it. If this field is user. If this field is set, credentials can only be created for
set, then credentials can only be created for default_user and usernames the values in this list and the value of the `default_user` field.
present in this list.
</li> </li>
<li> <li>
<span class="param">key_option_specs</span> <span class="param">key_option_specs</span>
<span class="param-flags">optional for Dynamic type, NA for OTP type</span> <span class="param-flags">optional for Dynamic Key type, N/A for OTP type</span>
(String) (String)
Comma separated option specifications which will be prefixed to RSA key in Comma separated option specification which will be prefixed to RSA
authorized_keys file. Options should be valid and comply with authorized_keys keys in the remote host's authorized_keys file. N.B.: Vault does
file format and should not contain spaces. not check this string for validity.
</li> </li>
</ul> </ul>
</dd> </dd>
@@ -522,7 +464,7 @@ username@ip:~$
<dd>None</dd> <dd>None</dd>
<dt>Returns</dt> <dt>Returns</dt>
<dd>For dynamic role: <dd>For a dynamic key role:
```json ```json
{ {
@@ -536,7 +478,7 @@ username@ip:~$
``` ```
</dd> </dd>
<dd>For OTP role: <dd>For an OTP role:
```json ```json
{ {
@@ -576,7 +518,8 @@ username@ip:~$
<dl class="api"> <dl class="api">
<dt>Description</dt> <dt>Description</dt>
<dd> <dd>
Creates a credential for a specific username and IP under the given role. Creates credentials for a specific username and IP with the
parameters defined in the given role.
</dd> </dd>
<dt>Method</dt> <dt>Method</dt>
@@ -592,7 +535,7 @@ username@ip:~$
<span class="param">username</span> <span class="param">username</span>
<span class="param-flags">optional</span> <span class="param-flags">optional</span>
(String) (String)
Username in remote host. Username on the remote host.
</li> </li>
<li> <li>
<span class="param">ip</span> <span class="param">ip</span>
@@ -614,7 +557,7 @@ username@ip:~$
<dl class="api"> <dl class="api">
<dt>Description</dt> <dt>Description</dt>
<dd> <dd>
Lists all the roles given IP is associated with. Lists all of the roles with which the given IP is associated.
</dd> </dd>
<dt>Method</dt> <dt>Method</dt>
@@ -646,7 +589,8 @@ username@ip:~$
<dl class="api"> <dl class="api">
<dt>Description</dt> <dt>Description</dt>
<dd> <dd>
Verifies if the given OTP is valid. This is an unauthenticated endpoint. Verifies if the given OTP is valid. This is an unauthenticated
endpoint.
</dd> </dd>
<dt>Method</dt> <dt>Method</dt>
@@ -671,4 +615,3 @@ username@ip:~$
<dd> <dd>
A `204` response code. A `204` response code.
</dd> </dd>