Vault SSH: Review Rework
This commit is contained in:
parent
e6a99525dc
commit
61c9f884a4
22
api/ssh.go
22
api/ssh.go
|
@ -4,27 +4,21 @@ import "fmt"
|
|||
|
||||
// SSH is used to return a client to invoke operations on SSH backend.
|
||||
type SSH struct {
|
||||
c *Client
|
||||
c *Client
|
||||
Path string
|
||||
}
|
||||
|
||||
// SSH is used to return the client for logical-backend API calls.
|
||||
func (c *Client) SSH() *SSH {
|
||||
return &SSH{c: c}
|
||||
}
|
||||
|
||||
// Invokes the SSH backend API to revoke a key identified by its lease ID.
|
||||
func (c *SSH) KeyRevoke(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()
|
||||
func (c *Client) SSH(path string) *SSH {
|
||||
return &SSH{
|
||||
c: c,
|
||||
Path: path,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Invokes the SSH backend API to create a dynamic key or an OTP
|
||||
func (c *SSH) KeyCreate(role string, data map[string]interface{}) (*Secret, error) {
|
||||
r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/ssh/creds/%s", role))
|
||||
func (c *SSH) Credential(role string, data map[string]interface{}) (*Secret, error) {
|
||||
r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/%s/creds/%s", c.Path, role))
|
||||
if err := r.SetJSONBody(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@ import (
|
|||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
salt *salt.Salt
|
||||
}
|
||||
|
||||
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b, err := Backend(conf)
|
||||
if err != nil {
|
||||
|
@ -54,11 +59,6 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
|
|||
return b.Backend, nil
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
salt *salt.Salt
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The SSH backend generates keys to eatablish SSH connection
|
||||
with remote hosts. There are two options to create the keys:
|
||||
|
|
|
@ -8,6 +8,11 @@ import (
|
|||
"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",
|
||||
|
@ -89,11 +94,6 @@ func (b *backend) Lease(s logical.Storage) (*configLease, error) {
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
type configLease struct {
|
||||
Lease time.Duration
|
||||
LeaseMax time.Duration
|
||||
}
|
||||
|
||||
const pathConfigLeaseHelpSyn = `
|
||||
Configure the default lease information for SSH dynamic keys.
|
||||
`
|
||||
|
|
|
@ -3,12 +3,22 @@ package ssh
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/vault/helper/uuid"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
type sshOTP struct {
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
type sshCIDR struct {
|
||||
CIDR []string
|
||||
}
|
||||
|
||||
func pathCredsCreate(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "creds/(?P<name>[-\\w]+)",
|
||||
|
@ -85,25 +95,10 @@ func (b *backend) pathCredsCreateWrite(
|
|||
var result *logical.Response
|
||||
if role.KeyType == KeyTypeOTP {
|
||||
// Generate salted OTP
|
||||
otp := uuid.GenerateUUID()
|
||||
otpSalted := b.salt.SaltID(otp)
|
||||
entry, err := req.Storage.Get("otp/" + otpSalted)
|
||||
// Make sure that new OTP is not replacing an existing one
|
||||
for err == nil && entry != nil {
|
||||
otp := uuid.GenerateUUID()
|
||||
otpSalted := b.salt.SaltID(otp)
|
||||
entry, err = req.Storage.Get("otp/" + otpSalted)
|
||||
}
|
||||
entry, err = logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
|
||||
Username: username,
|
||||
IP: ip,
|
||||
})
|
||||
otp, err := b.GenerateOTPCredential(req, username, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = b.Secret(SecretOTPType).Response(map[string]interface{}{
|
||||
"key_type": role.KeyType,
|
||||
"key": otp,
|
||||
|
@ -111,39 +106,10 @@ func (b *backend) pathCredsCreateWrite(
|
|||
"otp": otp,
|
||||
})
|
||||
} else if role.KeyType == KeyTypeDynamic {
|
||||
// Fetch the host key to be used for dynamic key installation
|
||||
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", role.KeyName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("key '%s' not found error:%s", role.KeyName, err)
|
||||
}
|
||||
|
||||
if keyEntry == nil {
|
||||
return nil, fmt.Errorf("key '%s' not found", role.KeyName, err)
|
||||
}
|
||||
|
||||
var hostKey sshHostKey
|
||||
if err := keyEntry.DecodeJSON(&hostKey); err != nil {
|
||||
return nil, fmt.Errorf("error reading the host key: %s", err)
|
||||
}
|
||||
|
||||
// Generate RSA key pair
|
||||
dynamicPublicKey, dynamicPrivateKey, err := generateRSAKeys()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating key: %s", err)
|
||||
}
|
||||
|
||||
// Transfer the public key to target machine
|
||||
err = uploadPublicKeyScp(dynamicPublicKey, username, ip, role.Port, hostKey.Key)
|
||||
dynamicPublicKey, dynamicPrivateKey, err := b.GenerateDynamicCredential(req, &role, username, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the public key to authorized_keys file in target machine
|
||||
err = installPublicKeyInTarget(role.AdminUser, username, ip, role.Port, hostKey.Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error adding public key to authorized_keys file in target")
|
||||
}
|
||||
|
||||
result = b.Secret(SecretDynamicKeyType).Response(map[string]interface{}{
|
||||
"key": dynamicPrivateKey,
|
||||
"key_type": role.KeyType,
|
||||
|
@ -168,13 +134,74 @@ func (b *backend) pathCredsCreateWrite(
|
|||
return result, nil
|
||||
}
|
||||
|
||||
type sshOTP struct {
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
// Generates a RSA key pair and installs it in the remote target
|
||||
func (b *backend) GenerateDynamicCredential(req *logical.Request, role *sshRole, username, ip string) (string, string, error) {
|
||||
// Fetch the host key to be used for dynamic key installation
|
||||
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", role.KeyName))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("key '%s' not found error:%s", role.KeyName, err)
|
||||
}
|
||||
|
||||
if keyEntry == nil {
|
||||
return "", "", fmt.Errorf("key '%s' not found", role.KeyName, err)
|
||||
}
|
||||
|
||||
var hostKey sshHostKey
|
||||
if err := keyEntry.DecodeJSON(&hostKey); err != nil {
|
||||
return "", "", fmt.Errorf("error reading the host key: %s", err)
|
||||
}
|
||||
|
||||
// Generate RSA key pair
|
||||
keyBits, err := strconv.Atoi(role.KeyBits)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error reading key bit size: %s", err)
|
||||
}
|
||||
|
||||
dynamicPublicKey, dynamicPrivateKey, err := generateRSAKeys(keyBits)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error generating key: %s", err)
|
||||
}
|
||||
|
||||
// Transfer the public key to target machine
|
||||
publicKeyFileName := uuid.GenerateUUID()
|
||||
err = uploadPublicKeyScp(dynamicPublicKey, publicKeyFileName, username, ip, role.Port, hostKey.Key)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Add the public key to authorized_keys file in target machine
|
||||
err = installPublicKeyInTarget(role.AdminUser, publicKeyFileName, username, ip, role.Port, hostKey.Key)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error adding public key to authorized_keys file in target")
|
||||
}
|
||||
return dynamicPublicKey, dynamicPrivateKey, nil
|
||||
}
|
||||
|
||||
type sshCIDR struct {
|
||||
CIDR []string
|
||||
// Generates an OTP and creates an entry for the same in storage backend.
|
||||
func (b *backend) GenerateOTPCredential(req *logical.Request, username, ip string) (string, error) {
|
||||
otp := uuid.GenerateUUID()
|
||||
otpSalted := b.salt.SaltID(otp)
|
||||
entry, err := req.Storage.Get("otp/" + otpSalted)
|
||||
// Make sure that new OTP is not replacing an existing one
|
||||
for err == nil && entry != nil {
|
||||
otp = uuid.GenerateUUID()
|
||||
otpSalted = b.salt.SaltID(otp)
|
||||
entry, err = req.Storage.Get("otp/" + otpSalted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
entry, err = logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
|
||||
Username: username,
|
||||
IP: ip,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return otp, nil
|
||||
}
|
||||
|
||||
const pathCredsCreateHelpSyn = `
|
||||
|
|
|
@ -3,10 +3,16 @@ package ssh
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
type sshHostKey struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func pathKeys(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "keys/(?P<name>[-\\w]+)",
|
||||
|
@ -62,6 +68,11 @@ func (b *backend) pathKeysWrite(req *logical.Request, d *framework.FieldData) (*
|
|||
keyName := d.Get("name").(string)
|
||||
keyString := d.Get("key").(string)
|
||||
|
||||
signer, err := ssh.ParsePrivateKey([]byte(keyString))
|
||||
if err != nil || signer == nil {
|
||||
return logical.ErrorResponse("Invalid key"), nil
|
||||
}
|
||||
|
||||
if keyString == "" {
|
||||
return logical.ErrorResponse("Missing key"), nil
|
||||
}
|
||||
|
@ -80,10 +91,6 @@ func (b *backend) pathKeysWrite(req *logical.Request, d *framework.FieldData) (*
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
type sshHostKey struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
const pathKeysSyn = `
|
||||
Register a shared key which can be used to install dynamic key
|
||||
in remote machine.
|
||||
|
|
|
@ -3,12 +3,17 @@ package ssh
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
const KeyTypeOTP = "otp"
|
||||
const KeyTypeDynamic = "dynamic"
|
||||
const KeyBitsRSA = "2048"
|
||||
|
||||
func pathRoles(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "roles/(?P<name>[-\\w]+)",
|
||||
|
@ -41,6 +46,10 @@ func pathRoles(b *backend) *framework.Path {
|
|||
Type: framework.TypeString,
|
||||
Description: "one-time-password or dynamic-key",
|
||||
},
|
||||
"key_bits": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "number of bits in keys",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
|
@ -54,127 +63,104 @@ func pathRoles(b *backend) *framework.Path {
|
|||
}
|
||||
}
|
||||
|
||||
func createOTPRole(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
roleName := d.Get("name").(string)
|
||||
if roleName == "" {
|
||||
return logical.ErrorResponse("Missing role name"), nil
|
||||
}
|
||||
|
||||
cidr := d.Get("cidr").(string)
|
||||
if cidr == "" {
|
||||
return logical.ErrorResponse("Missing cidr blocks"), nil
|
||||
}
|
||||
for _, item := range strings.Split(cidr, ",") {
|
||||
_, _, err := net.ParseCIDR(item)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr entry '%s'", item)), nil
|
||||
}
|
||||
}
|
||||
|
||||
adminUser := d.Get("admin_user").(string)
|
||||
defaultUser := d.Get("default_user").(string)
|
||||
if defaultUser == "" && adminUser != "" {
|
||||
defaultUser = adminUser
|
||||
}
|
||||
|
||||
port := d.Get("port").(string)
|
||||
if port == "" {
|
||||
port = "22"
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
|
||||
AdminUser: adminUser,
|
||||
DefaultUser: defaultUser,
|
||||
CIDR: cidr,
|
||||
Port: port,
|
||||
KeyType: KeyTypeOTP,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func createDynamicKeyRole(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
roleName := d.Get("name").(string)
|
||||
if roleName == "" {
|
||||
return logical.ErrorResponse("Missing role name"), nil
|
||||
}
|
||||
|
||||
keyName := d.Get("key").(string)
|
||||
if keyName == "" {
|
||||
return logical.ErrorResponse("Missing key name"), nil
|
||||
}
|
||||
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", keyName))
|
||||
if err != nil || keyEntry == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Invalid 'key': '%s'", keyName)), nil
|
||||
}
|
||||
|
||||
adminUser := d.Get("admin_user").(string)
|
||||
if adminUser == "" {
|
||||
return logical.ErrorResponse("Missing admin username"), nil
|
||||
}
|
||||
|
||||
cidr := d.Get("cidr").(string)
|
||||
if cidr == "" {
|
||||
return logical.ErrorResponse("Missing cidr blocks"), nil
|
||||
}
|
||||
for _, item := range strings.Split(cidr, ",") {
|
||||
_, _, err := net.ParseCIDR(item)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr entry '%s'", item)), nil
|
||||
}
|
||||
}
|
||||
|
||||
defaultUser := d.Get("default_user").(string)
|
||||
if defaultUser == "" {
|
||||
defaultUser = adminUser
|
||||
}
|
||||
|
||||
port := d.Get("port").(string)
|
||||
if port == "" {
|
||||
port = "22"
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
|
||||
KeyName: keyName,
|
||||
AdminUser: adminUser,
|
||||
DefaultUser: defaultUser,
|
||||
CIDR: cidr,
|
||||
Port: port,
|
||||
KeyType: KeyTypeDynamic,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
roleName := d.Get("name").(string)
|
||||
if roleName == "" {
|
||||
return logical.ErrorResponse("Missing role name"), nil
|
||||
}
|
||||
|
||||
cidr := d.Get("cidr").(string)
|
||||
if cidr == "" {
|
||||
return logical.ErrorResponse("Missing cidr blocks"), nil
|
||||
}
|
||||
for _, item := range strings.Split(cidr, ",") {
|
||||
_, _, err := net.ParseCIDR(item)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr entry '%s'", item)), nil
|
||||
}
|
||||
}
|
||||
|
||||
port := d.Get("port").(string)
|
||||
if port == "" {
|
||||
port = "22"
|
||||
}
|
||||
|
||||
keyType := d.Get("key_type").(string)
|
||||
if keyType == "" {
|
||||
return logical.ErrorResponse("Missing key type"), nil
|
||||
}
|
||||
keyType = strings.ToLower(keyType)
|
||||
|
||||
var entry *logical.StorageEntry
|
||||
var err error
|
||||
if keyType == KeyTypeOTP {
|
||||
return createOTPRole(req, d)
|
||||
adminUser := d.Get("admin_user").(string)
|
||||
if adminUser != "" {
|
||||
return logical.ErrorResponse("Admin user not required for OTP type"), nil
|
||||
}
|
||||
|
||||
defaultUser := d.Get("default_user").(string)
|
||||
if defaultUser == "" {
|
||||
return logical.ErrorResponse("Missing default user"), nil
|
||||
}
|
||||
|
||||
entry, err = logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
|
||||
DefaultUser: defaultUser,
|
||||
CIDR: cidr,
|
||||
KeyType: KeyTypeOTP,
|
||||
})
|
||||
} else if keyType == KeyTypeDynamic {
|
||||
return createDynamicKeyRole(req, d)
|
||||
keyName := d.Get("key").(string)
|
||||
if keyName == "" {
|
||||
return logical.ErrorResponse("Missing key name"), nil
|
||||
}
|
||||
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", keyName))
|
||||
if err != nil || keyEntry == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("Invalid 'key': '%s'", keyName)), nil
|
||||
}
|
||||
|
||||
adminUser := d.Get("admin_user").(string)
|
||||
if adminUser == "" {
|
||||
return logical.ErrorResponse("Missing admin username"), nil
|
||||
}
|
||||
|
||||
defaultUser := d.Get("default_user").(string)
|
||||
if defaultUser == "" {
|
||||
defaultUser = adminUser
|
||||
}
|
||||
|
||||
keyBits := d.Get("key_bits").(string)
|
||||
if keyBits != "" {
|
||||
_, err := strconv.Atoi(keyBits)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("Key bits should be an integer"), nil
|
||||
}
|
||||
}
|
||||
if keyBits == "" {
|
||||
keyBits = KeyBitsRSA
|
||||
}
|
||||
|
||||
entry, err = logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
|
||||
KeyName: keyName,
|
||||
AdminUser: adminUser,
|
||||
DefaultUser: defaultUser,
|
||||
CIDR: cidr,
|
||||
Port: port,
|
||||
KeyType: KeyTypeDynamic,
|
||||
KeyBits: keyBits,
|
||||
})
|
||||
} else {
|
||||
return logical.ErrorResponse("Invalid key type"), nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRoleRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
|
@ -216,15 +202,13 @@ func (b *backend) pathRoleDelete(req *logical.Request, d *framework.FieldData) (
|
|||
type sshRole struct {
|
||||
KeyType string `json:"key_type"`
|
||||
KeyName string `json:"key"`
|
||||
KeyBits string `json:"key_bits"`
|
||||
AdminUser string `json:"admin_user"`
|
||||
DefaultUser string `json:"default_user"`
|
||||
CIDR string `json:"cidr"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
const KeyTypeOTP = "otp"
|
||||
const KeyTypeDynamic = "dynamic"
|
||||
|
||||
const pathRoleHelpSyn = `
|
||||
Manage the 'roles' that can be created with this backend.
|
||||
`
|
||||
|
|
|
@ -41,11 +41,11 @@ func (b *backend) pathVerifyWrite(req *logical.Request, d *framework.FieldData)
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"username": otpEntry.Username,
|
||||
"ip": otpEntry.IP,
|
||||
"valid": "yes",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/helper/uuid"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
@ -24,7 +25,7 @@ func secretDynamicKey(b *backend) *framework.Secret {
|
|||
},
|
||||
},
|
||||
DefaultDuration: 10 * time.Minute,
|
||||
DefaultGracePeriod: 5 * time.Minute,
|
||||
DefaultGracePeriod: 2 * time.Minute,
|
||||
Renew: b.secretDynamicKeyRenew,
|
||||
Revoke: b.secretDynamicKeyRevoke,
|
||||
}
|
||||
|
@ -105,13 +106,14 @@ func (b *backend) secretDynamicKeyRevoke(req *logical.Request, d *framework.Fiel
|
|||
}
|
||||
|
||||
// Transfer the dynamic public key to target machine and use it to remove the entry from authorized_keys file
|
||||
err = uploadPublicKeyScp(dynamicPublicKey, username, ip, port, hostKey.Key)
|
||||
dynamicPublicKeyFileName := uuid.GenerateUUID()
|
||||
err = uploadPublicKeyScp(dynamicPublicKey, dynamicPublicKeyFileName, username, ip, port, hostKey.Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("public key transfer failed: %s", err)
|
||||
}
|
||||
|
||||
// Remove the public key from authorized_keys file in target machine
|
||||
err = uninstallPublicKeyInTarget(adminUser, username, ip, port, hostKey.Key)
|
||||
err = uninstallPublicKeyInTarget(adminUser, dynamicPublicKeyFileName, username, ip, port, hostKey.Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error removing public key from authorized_keys file in target")
|
||||
}
|
||||
|
|
|
@ -20,24 +20,11 @@ func secretOTP(b *backend) *framework.Secret {
|
|||
},
|
||||
},
|
||||
DefaultDuration: 10 * time.Minute,
|
||||
DefaultGracePeriod: 5 * time.Minute,
|
||||
Renew: b.secretOTPRenew,
|
||||
DefaultGracePeriod: 2 * time.Minute,
|
||||
Revoke: b.secretOTPRevoke,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) secretOTPRenew(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
lease, err := b.Lease(req.Storage)
|
||||
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)
|
||||
}
|
||||
|
||||
func (b *backend) secretOTPRevoke(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
otpRaw, ok := req.Secret.InternalData["otp"]
|
||||
if !ok {
|
||||
|
|
|
@ -20,8 +20,7 @@ import (
|
|||
// session with the target. Uses the public key authentication method
|
||||
// and hence the parameter 'key' takes in the private key. The fileName
|
||||
// parameter takes an absolute path.
|
||||
func uploadPublicKeyScp(publicKey, username, ip, port, key string) error {
|
||||
dynamicPublicKeyFileName := fmt.Sprintf("vault_ssh_%s_%s.pub", username, ip)
|
||||
func uploadPublicKeyScp(publicKey, publicKeyFileName, username, ip, port, key string) error {
|
||||
session, err := createSSHPublicKeysSession(username, ip, port, key)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -32,12 +31,12 @@ func uploadPublicKeyScp(publicKey, username, ip, port, key string) error {
|
|||
defer session.Close()
|
||||
go func() {
|
||||
w, _ := session.StdinPipe()
|
||||
fmt.Fprintln(w, "C0644", len(publicKey), dynamicPublicKeyFileName)
|
||||
fmt.Fprintln(w, "C0644", len(publicKey), publicKeyFileName)
|
||||
io.Copy(w, strings.NewReader(publicKey))
|
||||
fmt.Fprint(w, "\x00")
|
||||
w.Close()
|
||||
}()
|
||||
err = session.Run(fmt.Sprintf("scp -vt %s", dynamicPublicKeyFileName))
|
||||
err = session.Run(fmt.Sprintf("scp -vt %s", publicKeyFileName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("public key upload failed")
|
||||
}
|
||||
|
@ -87,8 +86,8 @@ func createSSHPublicKeysSession(username, ipAddr, port, hostKey string) (*ssh.Se
|
|||
// Creates a new RSA key pair with key length of 2048.
|
||||
// The private key will be of pem format and the public key will be
|
||||
// of OpenSSH format.
|
||||
func generateRSAKeys() (publicKeyRsa string, privateKeyRsa string, err error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, err error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error generating RSA key-pair: %s", err)
|
||||
}
|
||||
|
@ -108,7 +107,7 @@ func generateRSAKeys() (publicKeyRsa string, privateKeyRsa string, err error) {
|
|||
|
||||
// Concatenates the public present in that target machine's home
|
||||
// folder to ~/.ssh/authorized_keys file
|
||||
func installPublicKeyInTarget(adminUser, username, ip, port, hostKey string) error {
|
||||
func installPublicKeyInTarget(adminUser, publicKeyFileName, username, ip, port, hostKey string) error {
|
||||
session, err := createSSHPublicKeysSession(adminUser, ip, port, hostKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create SSH Session using public keys: %s", err)
|
||||
|
@ -122,11 +121,10 @@ func installPublicKeyInTarget(adminUser, username, ip, port, hostKey string) err
|
|||
tempKeysFileName := fmt.Sprintf("/home/%s/temp_authorized_keys", username)
|
||||
|
||||
// Commands to be run on target machine
|
||||
dynamicPublicKeyFileName := fmt.Sprintf("vault_ssh_%s_%s.pub", username, ip)
|
||||
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", dynamicPublicKeyFileName, authKeysFileName, tempKeysFileName)
|
||||
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", publicKeyFileName, authKeysFileName, tempKeysFileName)
|
||||
catCmdRemoveDuplicate := fmt.Sprintf("cat %s > %s", tempKeysFileName, authKeysFileName)
|
||||
catCmdAppendNew := fmt.Sprintf("cat %s >> %s", dynamicPublicKeyFileName, authKeysFileName)
|
||||
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, dynamicPublicKeyFileName)
|
||||
catCmdAppendNew := fmt.Sprintf("cat %s >> %s", publicKeyFileName, authKeysFileName)
|
||||
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, publicKeyFileName)
|
||||
|
||||
targetCmd := fmt.Sprintf("%s;%s;%s;%s", grepCmd, catCmdRemoveDuplicate, catCmdAppendNew, removeCmd)
|
||||
session.Run(targetCmd)
|
||||
|
@ -135,7 +133,7 @@ func installPublicKeyInTarget(adminUser, username, ip, port, hostKey string) err
|
|||
|
||||
// Removes the installed public key from the authorized_keys file
|
||||
// in target machine
|
||||
func uninstallPublicKeyInTarget(adminUser, username, ip, port, hostKey string) error {
|
||||
func uninstallPublicKeyInTarget(adminUser, publicKeyFileName, username, ip, port, hostKey string) error {
|
||||
session, err := createSSHPublicKeysSession(adminUser, ip, port, hostKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create SSH Session using public keys: %s", err)
|
||||
|
@ -149,10 +147,9 @@ func uninstallPublicKeyInTarget(adminUser, username, ip, port, hostKey string) e
|
|||
tempKeysFileName := fmt.Sprintf("/home/%s/temp_authorized_keys", username)
|
||||
|
||||
// Commands to be run on target machine
|
||||
dynamicPublicKeyFileName := fmt.Sprintf("vault_ssh_%s_%s.pub", username, ip)
|
||||
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", dynamicPublicKeyFileName, authKeysFileName, tempKeysFileName)
|
||||
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", publicKeyFileName, authKeysFileName, tempKeysFileName)
|
||||
catCmdRemoveDuplicate := fmt.Sprintf("cat %s > %s", tempKeysFileName, authKeysFileName)
|
||||
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, dynamicPublicKeyFileName)
|
||||
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, publicKeyFileName)
|
||||
|
||||
remoteCmd := fmt.Sprintf("%s;%s;%s", grepCmd, catCmdRemoveDuplicate, removeCmd)
|
||||
session.Run(remoteCmd)
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/logical/ssh"
|
||||
)
|
||||
|
||||
|
@ -19,13 +19,16 @@ type SSHCommand struct {
|
|||
}
|
||||
|
||||
func (c *SSHCommand) Run(args []string) int {
|
||||
var role string
|
||||
var port string
|
||||
var role, port, path string
|
||||
var noExec bool
|
||||
var sshCmdArgs []string
|
||||
var sshDynamicKeyFileName string
|
||||
flags := c.Meta.FlagSet("ssh", FlagSetDefault)
|
||||
flags.StringVar(&role, "role", "", "")
|
||||
flags.StringVar(&port, "port", "22", "")
|
||||
flags.StringVar(&path, "path", "ssh", "")
|
||||
flags.BoolVar(&noExec, "no-exec", false, "")
|
||||
|
||||
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
|
@ -43,23 +46,33 @@ func (c *SSHCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
input := strings.Split(args[0], "@")
|
||||
if len(input) != 2 {
|
||||
var username string
|
||||
var ipAddr string
|
||||
if len(input) == 1 {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error fetching username: '%s'", err))
|
||||
}
|
||||
username = u.Username
|
||||
ipAddr = input[0]
|
||||
} else if len(input) == 2 {
|
||||
username = input[0]
|
||||
ipAddr = input[1]
|
||||
} else {
|
||||
c.Ui.Error(fmt.Sprintf("Invalid parameter: %s", args[0]))
|
||||
return 2
|
||||
}
|
||||
|
||||
username := input[0]
|
||||
|
||||
ip, err := net.ResolveIPAddr("ip", input[1])
|
||||
ip, err := net.ResolveIPAddr("ip", ipAddr)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error resolving IP Address: %s", err))
|
||||
return 2
|
||||
}
|
||||
|
||||
if role == "" {
|
||||
role, err = setDefaultRole(client, ip.String())
|
||||
role, err = c.defaultRole(path, ip.String())
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error setting default role: %s", err.Error()))
|
||||
c.Ui.Error(fmt.Sprintf("Error setting default role: '%s'", err))
|
||||
return 1
|
||||
}
|
||||
c.Ui.Output(fmt.Sprintf("Vault SSH: Role:'%s'\n", role))
|
||||
|
@ -70,12 +83,17 @@ func (c *SSHCommand) Run(args []string) int {
|
|||
"ip": ip.String(),
|
||||
}
|
||||
|
||||
keySecret, err := client.SSH().KeyCreate(role, data)
|
||||
keySecret, err := client.SSH(path).Credential(role, data)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error getting key for SSH session:%s", err))
|
||||
return 2
|
||||
}
|
||||
|
||||
if noExec {
|
||||
c.Ui.Output(fmt.Sprintf("IP:%s\nUsername: %s\nKey:%s\n", ip.String(), username, keySecret.Data["key"]))
|
||||
return 0
|
||||
}
|
||||
|
||||
if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic {
|
||||
sshDynamicKey := string(keySecret.Data["key"].(string))
|
||||
if len(sshDynamicKey) == 0 {
|
||||
|
@ -87,9 +105,8 @@ func (c *SSHCommand) Run(args []string) int {
|
|||
sshCmdArgs = append(sshCmdArgs, []string{"-i", sshDynamicKeyFileName}...)
|
||||
|
||||
} else if keySecret.Data["key_type"].(string) == ssh.KeyTypeOTP {
|
||||
fmt.Printf("OTP for the session is %s\n", string(keySecret.Data["key"].(string)))
|
||||
c.Ui.Output(fmt.Sprintf("OTP for the session is %s\n", string(keySecret.Data["key"].(string))))
|
||||
} else {
|
||||
// Intentionally not mentioning the exact error
|
||||
c.Ui.Error("Error creating key")
|
||||
}
|
||||
sshCmdArgs = append(sshCmdArgs, []string{"-p", port}...)
|
||||
|
@ -107,25 +124,30 @@ func (c *SSHCommand) Run(args []string) int {
|
|||
if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic {
|
||||
err = os.Remove(sshDynamicKeyFileName)
|
||||
if err != nil {
|
||||
// Intentionally not mentioning the exact error
|
||||
c.Ui.Error("Error cleaning up")
|
||||
c.Ui.Error(fmt.Sprintf("Error deleting key file: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
err = client.SSH().KeyRevoke(keySecret.LeaseID)
|
||||
err = client.Sys().Revoke(keySecret.LeaseID)
|
||||
if err != nil {
|
||||
// Intentionally not mentioning the exact error
|
||||
c.Ui.Error("Error cleaning up")
|
||||
c.Ui.Error(fmt.Sprintf("Error revoking the key: %s", err))
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func setDefaultRole(client *api.Client, ip string) (string, error) {
|
||||
// If user did not provide the role with which SSH connection has
|
||||
// to be established and if there is only one role associated with
|
||||
// the IP, it is used by default.
|
||||
func (c *SSHCommand) defaultRole(path, ip string) (string, error) {
|
||||
data := map[string]interface{}{
|
||||
"ip": ip,
|
||||
}
|
||||
secret, err := client.Logical().Write("ssh/lookup", data)
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secret, err := client.Logical().Write(path+"/lookup", data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error finding roles for IP '%s':%s", ip, err)
|
||||
|
||||
|
@ -135,13 +157,18 @@ func setDefaultRole(client *api.Client, ip string) (string, error) {
|
|||
}
|
||||
|
||||
if secret.Data["roles"] == nil {
|
||||
return "", fmt.Errorf("IP '%s' not registered under any role", ip)
|
||||
return "", fmt.Errorf("No matching roles found for IP '%s'", ip)
|
||||
}
|
||||
|
||||
if len(secret.Data["roles"].([]interface{})) == 1 {
|
||||
return secret.Data["roles"].([]interface{})[0].(string), nil
|
||||
} else {
|
||||
return "", fmt.Errorf("Multiple roles for IP '%s'. Select one of '%s' using '-role' option", ip, secret.Data["roles"])
|
||||
var roleNames string
|
||||
for _, item := range secret.Data["roles"].([]interface{}) {
|
||||
roleNames += item.(string) + ", "
|
||||
}
|
||||
roleNames = strings.TrimRight(roleNames, ", ")
|
||||
return "", fmt.Errorf("IP '%s' has multiple roles.\nSelect a role using '-role' option.\nPossible roles: [%s]\nNote that all roles may not be permitted, based on ACLs.", ip, roleNames)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,21 +187,31 @@ Usage: vault ssh [options] username@ip
|
|||
that SSH backend is mounted and at least one 'role' be registed
|
||||
with vault at priori.
|
||||
|
||||
For setting up SSH backends with one-time-passwords, installation
|
||||
of agent in target machines is required.
|
||||
See [https://github.com/hashicorp/vault-ssh-agent]
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage() + `
|
||||
|
||||
SSH Options:
|
||||
|
||||
-role Mention the role to be used to create dynamic key.
|
||||
-role Role to be used to create the key.
|
||||
Each IP is associated with a role. To see the associated
|
||||
roles with IP, use "lookup" endpoint. If you are certain that
|
||||
there is only one role associated with the IP, you can
|
||||
skip mentioning the role. It will be chosen by default.
|
||||
If there are no roless associated with the IP, register
|
||||
If there are no roles associated with the IP, register
|
||||
the CIDR block of that IP using the "roles/" endpoint.
|
||||
|
||||
-port Port number to use for SSH connection. This defaults to port 22.
|
||||
|
||||
-no-exec Shows the credentials but does not establish connection.
|
||||
|
||||
-path Mount point of SSH backend. If the backend is mounted at
|
||||
'ssh', which is the default as well, this parameter can
|
||||
be skipped.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue