diff --git a/api/ssh.go b/api/ssh.go new file mode 100644 index 0000000000..cfe8c1bb67 --- /dev/null +++ b/api/ssh.go @@ -0,0 +1,26 @@ +package api + +import "fmt" + +type Ssh struct { + c *Client +} + +func (c *Client) Ssh() *Ssh { + return &Ssh{c: c} +} + +func (c *Ssh) KeyCreate(data map[string]interface{}) (*Secret, error) { + r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/ssh/creds/web")) + if err := r.SetJSONBody(data); err != nil { + return nil, err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} diff --git a/api/sys_ssh.go b/api/sys_ssh.go deleted file mode 100644 index bce918072d..0000000000 --- a/api/sys_ssh.go +++ /dev/null @@ -1,30 +0,0 @@ -package api - -import "fmt" - -func (c *Sys) Ssh(data map[string]interface{}) (*Secret, error) { - r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/ssh/creds/web")) - if err := r.SetJSONBody(data); err != nil { - return nil, err - } - - resp, err := c.c.RawRequest(r) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - return ParseSecret(resp.Body) - - /* - result := new(Secret) - err = resp.DecodeJSON(&result) - log.Printf("Vishal: api.sys_ssh.Ssh: result:%#v\n", result.Data) - - var oneTimeKey OneTimeKey - err = result.Data.DecodeJSON(&oneTimeKey) - log.Printf("Vishal: oneTimeKey:%#v\n", oneTimeKey) - return &oneTimeKey, err - */ - //return result, err -} diff --git a/builtin/logical/ssh/backend.go b/builtin/logical/ssh/backend.go index 33e414722c..99ed89fc49 100644 --- a/builtin/logical/ssh/backend.go +++ b/builtin/logical/ssh/backend.go @@ -27,6 +27,7 @@ func Backend() *framework.Backend { pathKeys(&b), pathRoles(&b), pathRoleCreate(&b), + pathLookup(&b), }, Secrets: []*framework.Secret{ diff --git a/builtin/logical/ssh/path_lookup.go b/builtin/logical/ssh/path_lookup.go new file mode 100644 index 0000000000..c65f759675 --- /dev/null +++ b/builtin/logical/ssh/path_lookup.go @@ -0,0 +1,85 @@ +package ssh + +import ( + "encoding/json" + "fmt" + "log" + "net" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathLookup(b *backend) *framework.Path { + log.Printf("Vishal: ssh.pathLookup\n") + return &framework.Path{ + Pattern: "lookup", + Fields: map[string]*framework.FieldSchema{ + "ip": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "IP address of target", + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathLookupWrite, + }, + HelpSynopsis: pathLookupSyn, + HelpDescription: pathLookupDesc, + } +} + +func (b *backend) pathLookupWrite(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + ip := d.Get("ip").(string) + //ip := "127.0.0.1" + ipAddr := net.ParseIP(ip) + if ipAddr == nil { + return logical.ErrorResponse(fmt.Sprintf("Invalid IP '%s'", ip)), nil + } + keys, _ := req.Storage.List("policy/") + var matchingRoles []string + for _, item := range keys { + if contains, _ := containsIP(req.Storage, item, ip); contains { + matchingRoles = append(matchingRoles, item) + } + } + log.Printf("Vishal: req.Path: %#v \n Keys:%#v\n", req.Path, keys) + return &logical.Response{ + Data: map[string]interface{}{ + "roles": matchingRoles, + }, + }, nil +} + +func containsIP(s logical.Storage, roleName string, ip string) (bool, error) { + roleEntry, err := s.Get("policy/" + roleName) + if err != nil { + return false, fmt.Errorf("error retrieving role '%s'", err) + } + if roleEntry == nil { + return false, fmt.Errorf("role '%s' not found", roleName) + } + var role sshRole + if err := roleEntry.DecodeJSON(&role); err != nil { + return false, fmt.Errorf("error decoding role '%s'", roleName) + } + var cidrEntry sshCIDR + ipMatched := false + json.Unmarshal([]byte(role.CIDR), &cidrEntry) + for _, item := range cidrEntry.CIDR { + log.Println(item) + _, cidrIPNet, _ := net.ParseCIDR(item) + ipMatched = cidrIPNet.Contains(net.ParseIP(ip)) + if ipMatched { + break + } + } + return ipMatched, nil +} + +const pathLookupSyn = ` +pathLookupSyn +` + +const pathLookupDesc = ` +pathLoookupDesc +` diff --git a/builtin/logical/ssh/path_role_create.go b/builtin/logical/ssh/path_role_create.go index c2afb60c4b..9e082393e5 100644 --- a/builtin/logical/ssh/path_role_create.go +++ b/builtin/logical/ssh/path_role_create.go @@ -2,9 +2,11 @@ package ssh import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "log" + "net" "strings" "github.com/hashicorp/vault/logical" @@ -41,10 +43,12 @@ func (b *backend) pathRoleCreateWrite( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { log.Printf("Vishal: ssh.pathRoleCreateWrite\n") + //fetch the parameters roleName := d.Get("name").(string) username := d.Get("username").(string) - ipAddr := d.Get("ip").(string) + ipRaw := d.Get("ip").(string) + //find the role to be used for installing dynamic key rolePath := "policy/" + roleName roleEntry, err := req.Storage.Get(rolePath) if err != nil { @@ -53,67 +57,119 @@ func (b *backend) pathRoleCreateWrite( if roleEntry == nil { return logical.ErrorResponse(fmt.Sprintf("Role '%s' not found", roleName)), nil } - var role sshRole if err := roleEntry.DecodeJSON(&role); err != nil { return nil, err } - log.Printf("Vishal: ssh.pathRoleCreateWrite username:%#v address:%#v name:%#v result:%s\n", username, ipAddr, roleName, role) - //TODO: do the role verification here + //validate the IP address + ipAddr := net.ParseIP(ipRaw) + if ipAddr == nil { + return logical.ErrorResponse(fmt.Sprintf("Invalid IP '%s'", ipRaw)), nil + } + ip := ipAddr.String() + var cidrEntry sshCIDR + ipMatched := false + json.Unmarshal([]byte(role.CIDR), &cidrEntry) + for _, item := range cidrEntry.CIDR { + log.Println(item) + _, cidrIPNet, _ := net.ParseCIDR(item) + ipMatched = cidrIPNet.Contains(ipAddr) + if ipMatched { + break + } + } + if !ipMatched { + return logical.ErrorResponse(fmt.Sprintf("IP[%s] does not belong to role[%s]", ip, roleName)), nil + } + + //fetch the host key to be used for installation keyPath := "keys/" + role.KeyName keyEntry, err := req.Storage.Get(keyPath) if err != nil { return nil, fmt.Errorf("Key '%s' not found error:%s", role.KeyName, err) } - - log.Printf("Vishal: KeyName:%s keyPath:%s\n", role.KeyName, keyPath) var hostKey sshHostKey if err := keyEntry.DecodeJSON(&hostKey); err != nil { return nil, fmt.Errorf("Error reading the host key: %s", err) } - log.Printf("Vishal: host key previously configured: \n---------------\n%#v\n--------------\n", hostKey.Key) - //TODO: Input validation for the commands below - hostKeyFileName := "./vault_ssh_" + username + "_" + ipAddr + "_shared.pem" + //store the host key to file. Use it as parameter for scp command + hostKeyFileName := "./vault_ssh_" + username + "_" + ip + "_shared.pem" err = ioutil.WriteFile(hostKeyFileName, []byte(hostKey.Key), 0400) - otkPrivateKeyFileName := "vault_ssh_" + username + "_" + ipAddr + "_otk.pem" + otkPrivateKeyFileName := "vault_ssh_" + username + "_" + ip + "_otk.pem" otkPublicKeyFileName := otkPrivateKeyFileName + ".pub" + + //commands to be run on vault server rmCmd := "rm -f " + otkPrivateKeyFileName + " " + otkPublicKeyFileName + ";" sshKeygenCmd := "ssh-keygen -f " + otkPrivateKeyFileName + " -t rsa -N ''" + ";" chmodCmd := "chmod 400 " + otkPrivateKeyFileName + ";" - scpCmd := "scp -i " + hostKeyFileName + " " + otkPublicKeyFileName + " " + username + "@" + ipAddr + ":~;" - - log.Printf("Vishal: scpCmd: \n", scpCmd) - + scpCmd := "scp -i " + hostKeyFileName + " " + otkPublicKeyFileName + " " + username + "@" + ip + ":~;" localCmdString := strings.Join([]string{ rmCmd, sshKeygenCmd, chmodCmd, scpCmd, }, "") + //run the commands on vault server err = exec_command(localCmdString) if err != nil { fmt.Errorf("Running command failed " + err.Error()) } - log.Printf("Vishal: Creating session\n") - session := createSSHPublicKeysSession(username, ipAddr) + + //connect to target machine + session := createSSHPublicKeysSession(username, ip, hostKey.Key) var buf bytes.Buffer session.Stdout = &buf - log.Printf("Vishal: Installing keys\n") - if err := installSshOtkInTarget(session, username, ipAddr); err != nil { - fmt.Errorf("Failed to install one-time-key at target machine: " + err.Error()) + + authKeysFileName := "~/.ssh/authorized_keys" + tempKeysFileName := "~/temp_authorized_keys" + + //commands to be run on target machine + grepCmd := "grep -vFf " + otkPublicKeyFileName + " " + authKeysFileName + " > " + tempKeysFileName + ";" + catCmdRemoveDuplicate := "cat " + tempKeysFileName + " > " + authKeysFileName + ";" + catCmdAppendNew := "cat " + otkPublicKeyFileName + " >> " + authKeysFileName + ";" + removeCmd := "rm -f " + tempKeysFileName + " " + otkPublicKeyFileName + ";" + remoteCmdString := strings.Join([]string{ + grepCmd, + catCmdRemoveDuplicate, + catCmdAppendNew, + removeCmd, + }, "") + + //run the commands on target machine + if err := session.Run(remoteCmdString); err != nil { + return nil, err } session.Close() fmt.Println(buf.String()) - keyBytes, err := ioutil.ReadFile(otkPrivateKeyFileName) - oneTimeKey := string(keyBytes) - log.Printf("Vishal: Returning:[%s]\n", oneTimeKey) + + //preparing the secret + dynamicPrivateKeyBytes, err := ioutil.ReadFile(otkPrivateKeyFileName) + if err != nil { + fmt.Errorf("Failed to open '%s':%s", otkPrivateKeyFileName, err) + } + dynamicPrivateKey := string(dynamicPrivateKeyBytes) + + dynamicPublicKeyBytes, err := ioutil.ReadFile(otkPublicKeyFileName) + if err != nil { + fmt.Errorf("Failed to open '%s':%s", otkPublicKeyFileName, err) + } + dynamicPublicKey := string(dynamicPublicKeyBytes) return b.Secret(SecretOneTimeKeyType).Response(map[string]interface{}{ - "key": oneTimeKey, - }, nil), nil + "key": dynamicPrivateKey, + }, map[string]interface{}{ + "username": username, + "ip": ip, + "host_key_name": role.KeyName, + "dynamic_public_key": dynamicPublicKey, + }), nil +} + +type sshCIDR struct { + CIDR []string } const sshConnectHelpSyn = ` diff --git a/builtin/logical/ssh/secret_ssh_key.go b/builtin/logical/ssh/secret_ssh_key.go index e1a644e7f1..d162b3c812 100644 --- a/builtin/logical/ssh/secret_ssh_key.go +++ b/builtin/logical/ssh/secret_ssh_key.go @@ -1,7 +1,11 @@ package ssh import ( + "bytes" + "fmt" + "io/ioutil" "log" + "strings" "time" "github.com/hashicorp/vault/logical" @@ -24,8 +28,8 @@ func secretSshKey(b *backend) *framework.Secret { Description: "ip address of host", }, }, - DefaultDuration: 1 * time.Hour, - DefaultGracePeriod: 10 * time.Minute, + DefaultDuration: 10 * time.Second, //TODO: change this + DefaultGracePeriod: 10 * time.Second, //TODO: change this Renew: b.secretSshKeyRenew, Revoke: b.secretSshKeyRevoke, } @@ -45,7 +49,90 @@ func (b *backend) secretSshKeyRenew(req *logical.Request, d *framework.FieldData } func (b *backend) secretSshKeyRevoke(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - log.Printf("Vishal: ssh.secretPrivateKeyRevoke\n") - //TODO: implement here + log.Printf("Vishal: ssh.secretPrivateKeyRevoke req: %#v\n", req) + //fetch the values from secret + usernameRaw, ok := req.Secret.InternalData["username"] + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + username, ok := usernameRaw.(string) + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + ipRaw, ok := req.Secret.InternalData["ip"] + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + ip, ok := ipRaw.(string) + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + hostKeyNameRaw, ok := req.Secret.InternalData["host_key_name"] + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + hostKeyName := hostKeyNameRaw.(string) + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + dynamicPublicKeyRaw, ok := req.Secret.InternalData["dynamic_public_key"] + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + dynamicPublicKey := dynamicPublicKeyRaw.(string) + if !ok { + return nil, fmt.Errorf("secret is missing internal data") + } + log.Printf("Vishal: username:%s ip:%s keyName:%s\n", username, ip, hostKeyName) + + //fetch the host key using the key name + hostKeyPath := "keys/" + hostKeyName + hostKeyEntry, err := req.Storage.Get(hostKeyPath) + if err != nil { + return nil, fmt.Errorf("Key '%s' not found error:%s", hostKeyName, err) + } + var hostKey sshHostKey + if err := hostKeyEntry.DecodeJSON(&hostKey); err != nil { + return nil, fmt.Errorf("Error reading the host key: %s", err) + } + + //write host key to file and use it as argument to scp command + hostKeyFileName := "./vault_ssh_" + username + "_" + ip + "_shared.pem" + err = ioutil.WriteFile(hostKeyFileName, []byte(hostKey.Key), 0400) + + //write dynamicPublicKey to file and use it as argument to scp command + otkPrivateKeyFileName := "vault_ssh_" + username + "_" + ip + "_otk.pem" + otkPublicKeyFileName := otkPrivateKeyFileName + ".pub" + err = ioutil.WriteFile(otkPublicKeyFileName, []byte(dynamicPublicKey), 0400) + + //transfer the dynamic public key to target machine and use it to remove the entry from authorized_keys file + scpCmd := "scp -i " + hostKeyFileName + " " + otkPublicKeyFileName + " " + username + "@" + ip + ":~;" + err = exec_command(scpCmd) + if err != nil { + fmt.Errorf("Running command scp failed " + err.Error()) + } + + authKeysFileName := "~/.ssh/authorized_keys" + tempKeysFileName := "~/temp_authorized_keys" + + //commands to be run on target machine + grepCmd := "grep -vFf " + otkPublicKeyFileName + " " + authKeysFileName + " > " + tempKeysFileName + ";" + catCmdRemoveDuplicate := "cat " + tempKeysFileName + " > " + authKeysFileName + ";" + rmCmd := "rm -f " + tempKeysFileName + " " + otkPublicKeyFileName + ";" + remoteCmdString := strings.Join([]string{ + grepCmd, + catCmdRemoveDuplicate, + rmCmd, + }, "") + + //connect to target machine + session := createSSHPublicKeysSession(username, ip, hostKey.Key) + var buf bytes.Buffer + session.Stdout = &buf + if err := session.Run(remoteCmdString); err != nil { + return nil, err + } + session.Close() + fmt.Println(buf.String()) return nil, nil } diff --git a/builtin/logical/ssh/ssh_util.go b/builtin/logical/ssh/ssh_util.go index f998330e17..bfa9aa489e 100644 --- a/builtin/logical/ssh/ssh_util.go +++ b/builtin/logical/ssh/ssh_util.go @@ -2,10 +2,7 @@ package ssh import ( "fmt" - "io/ioutil" - "log" "os/exec" - "strings" "golang.org/x/crypto/ssh" ) @@ -18,40 +15,8 @@ func exec_command(cmdString string) error { return nil } -func installSshOtkInTarget(session *ssh.Session, username string, ipAddr string) error { - log.Printf("Vishal: ssh.installSshOtkInTarget\n") - - //TODO: Input validation for the commands below - otkPrivateKeyFileName := "vault_ssh_" + username + "_" + ipAddr + "_otk.pem" - otkPublicKeyFileName := otkPrivateKeyFileName + ".pub" - authKeysFileName := "~/.ssh/authorized_keys" - tempKeysFileName := "./temp_authorized_keys" - - grepCmd := "grep -vFf " + otkPublicKeyFileName + " " + authKeysFileName + " > " + tempKeysFileName + ";" - catCmdRemoveDuplicate := "cat " + tempKeysFileName + " > " + authKeysFileName + ";" - catCmdAppendNew := "cat " + otkPublicKeyFileName + " >> " + authKeysFileName + ";" - rmCmd := "rm -f " + tempKeysFileName + " " + otkPublicKeyFileName + ";" - log.Printf("Vishal: grepCmd:%#v\n catCmdRemoveDuplicate:%#v\n catCmdAppendNew:%#v\n rmCmd: %#v\n", grepCmd, catCmdRemoveDuplicate, catCmdAppendNew, rmCmd) - remoteCmdString := strings.Join([]string{ - grepCmd, - catCmdRemoveDuplicate, - catCmdAppendNew, - rmCmd, - }, "") - - if err := session.Run(remoteCmdString); err != nil { - return err - } - return nil -} -func createSSHPublicKeysSession(username string, ipAddr string) *ssh.Session { - hostKeyFileName := "./vault_ssh_" + username + "_" + ipAddr + "_shared.pem" - pemBytes, err := ioutil.ReadFile(hostKeyFileName) - if err != nil { - fmt.Errorf("Reading shared key failed: " + err.Error()) - } - - signer, err := ssh.ParsePrivateKey(pemBytes) +func createSSHPublicKeysSession(username string, ipAddr string, hostKey string) *ssh.Session { + signer, err := ssh.ParsePrivateKey([]byte(hostKey)) if err != nil { fmt.Errorf("Parsing Private Key failed: " + err.Error()) } diff --git a/command/ssh.go b/command/ssh.go index f9752109ae..fd22f1b4dc 100644 --- a/command/ssh.go +++ b/command/ssh.go @@ -29,7 +29,10 @@ func (c *SshCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) return 2 } - //if len(args) < 3, fail + if len(args) < 1 { + c.Ui.Error(fmt.Sprintf("Insufficient arguments")) + return 2 + } log.Printf("Vishal: sshCommand.Run: args[0]: %#v\n", args[0]) input := strings.Split(args[0], "@") username := input[0] @@ -40,13 +43,13 @@ func (c *SshCommand) Run(args []string) int { "ip": ipAddr.String(), } - keySecret, err := client.Sys().Ssh(data) + keySecret, err := client.Ssh().KeyCreate(data) if err != nil { c.Ui.Error(fmt.Sprintf("Error getting key for establishing SSH session", err)) return 2 } sshOneTimeKey := string(keySecret.Data["key"].(string)) - log.Printf("Vishal: command.ssh.Run returned! OTK:%#v\n", sshOneTimeKey) + log.Printf("Vishal: command.ssh.Run returned! len(key):%d\n", len(sshOneTimeKey)) ag := strings.Split(args[0], "@") sshOtkFileName := "vault_ssh_otk_" + ag[0] + "_" + ag[1] + ".pem" err = ioutil.WriteFile(sshOtkFileName, []byte(sshOneTimeKey), 0400) diff --git a/http/sys_ssh.go b/http/sys_ssh.go deleted file mode 100644 index d739699871..0000000000 --- a/http/sys_ssh.go +++ /dev/null @@ -1,43 +0,0 @@ -package http - -import ( - "log" - "net/http" - - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/vault" -) - -func handleSysSsh(core *vault.Core) http.Handler { - log.Printf("Vishal: http.sys_ssh.handleSysSsh!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n") - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "PUT" { - respondError(w, http.StatusMethodNotAllowed, nil) - return - } - log.Printf("Vishal: http.sys_ssh.handleSysSsh: requesting\n") - var req SshRequest - if err := parseRequest(r, &req); err != nil { - respondError(w, http.StatusBadRequest, err) - return - } - - resp, ok := request(core, w, r, requestAuth(r, &logical.Request{ - Operation: logical.WriteOperation, - Path: "ssh/creds/web", - Data: map[string]interface{}{ - "username": req.Username, - "address": req.Address, - }, - })) - if !ok { - return - } - respondOk(w, resp.Data) - }) -} - -type SshRequest struct { - Username string `json: "username"` - Address string `json: "address"` -} diff --git a/vault/router.go b/vault/router.go index 0d6eaf7c3b..636df230bd 100644 --- a/vault/router.go +++ b/vault/router.go @@ -146,7 +146,6 @@ func (r *Router) MatchingView(path string) *BarrierView { // Route is used to route a given request func (r *Router) Route(req *logical.Request) (*logical.Response, error) { - log.Printf("Vishal: vault.router.Route: req.Path:%#v\n", req.Path) // Find the mount point r.l.RLock() mount, raw, ok := r.root.LongestPrefix(req.Path)