From 61c9f884a4b7d5697a57f21c89d39e052e623a94 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Wed, 29 Jul 2015 14:21:36 -0400 Subject: [PATCH] Vault SSH: Review Rework --- api/ssh.go | 22 +-- builtin/logical/ssh/backend.go | 10 +- builtin/logical/ssh/path_config_lease.go | 10 +- builtin/logical/ssh/path_creds_create.go | 129 ++++++++------ builtin/logical/ssh/path_keys.go | 15 +- builtin/logical/ssh/path_roles.go | 208 ++++++++++------------ builtin/logical/ssh/path_verify.go | 2 +- builtin/logical/ssh/secret_dynamic_key.go | 8 +- builtin/logical/ssh/secret_otp.go | 15 +- builtin/logical/ssh/util.go | 27 ++- command/ssh.go | 83 ++++++--- 11 files changed, 282 insertions(+), 247 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index cde775000..c683e1713 100644 --- a/api/ssh.go +++ b/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 } diff --git a/builtin/logical/ssh/backend.go b/builtin/logical/ssh/backend.go index b3d4addcb..cdc81b2e6 100644 --- a/builtin/logical/ssh/backend.go +++ b/builtin/logical/ssh/backend.go @@ -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: diff --git a/builtin/logical/ssh/path_config_lease.go b/builtin/logical/ssh/path_config_lease.go index b2f154410..ba6131a87 100644 --- a/builtin/logical/ssh/path_config_lease.go +++ b/builtin/logical/ssh/path_config_lease.go @@ -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. ` diff --git a/builtin/logical/ssh/path_creds_create.go b/builtin/logical/ssh/path_creds_create.go index bd59ebe6b..96afb7bfc 100644 --- a/builtin/logical/ssh/path_creds_create.go +++ b/builtin/logical/ssh/path_creds_create.go @@ -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[-\\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 = ` diff --git a/builtin/logical/ssh/path_keys.go b/builtin/logical/ssh/path_keys.go index 80834da87..eaca800a3 100644 --- a/builtin/logical/ssh/path_keys.go +++ b/builtin/logical/ssh/path_keys.go @@ -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[-\\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. diff --git a/builtin/logical/ssh/path_roles.go b/builtin/logical/ssh/path_roles.go index de4f035b3..e286aa296 100644 --- a/builtin/logical/ssh/path_roles.go +++ b/builtin/logical/ssh/path_roles.go @@ -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[-\\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. ` diff --git a/builtin/logical/ssh/path_verify.go b/builtin/logical/ssh/path_verify.go index e26b58287..33441a0c1 100644 --- a/builtin/logical/ssh/path_verify.go +++ b/builtin/logical/ssh/path_verify.go @@ -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 } diff --git a/builtin/logical/ssh/secret_dynamic_key.go b/builtin/logical/ssh/secret_dynamic_key.go index bd1872283..0136d0704 100644 --- a/builtin/logical/ssh/secret_dynamic_key.go +++ b/builtin/logical/ssh/secret_dynamic_key.go @@ -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") } diff --git a/builtin/logical/ssh/secret_otp.go b/builtin/logical/ssh/secret_otp.go index 5805cee96..1dc18b5f8 100644 --- a/builtin/logical/ssh/secret_otp.go +++ b/builtin/logical/ssh/secret_otp.go @@ -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 { diff --git a/builtin/logical/ssh/util.go b/builtin/logical/ssh/util.go index d5312f333..d40288a9f 100644 --- a/builtin/logical/ssh/util.go +++ b/builtin/logical/ssh/util.go @@ -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) diff --git a/command/ssh.go b/command/ssh.go index 1ef6d5f63..cdd095530 100644 --- a/command/ssh.go +++ b/command/ssh.go @@ -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) }