Skip to content

Commit

Permalink
feat: SSH-Agent Support (#306)
Browse files Browse the repository at this point in the history
* chore: add agent configuration bool

* feat: add ssh-agent authentication mechanism for linux

* chore: make sure ssh-agent auth is only executed on linux

* chore: add ssh user override

* chore: add ssh configuration block, check ssh config during VirtualEnvironmentClient creation

* fix: handle case of empty ssh config block

* chore: add ssh password auth fallback logic

* fix: remove not needed runtime

* fix linter errors & re-format

* allow ssh agent on all POSIX systems

* add `agent_socket` parameter

* update docs and examples

---------

Co-authored-by: zoop <zoop@zoop.li>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
  • Loading branch information
3 people committed May 22, 2023
1 parent 37494a0 commit 9fa9242
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 29 deletions.
46 changes: 45 additions & 1 deletion docs/index.md
Expand Up @@ -67,6 +67,35 @@ export PROXMOX_VE_PASSWORD="a-strong-password"
terraform plan
```

### SSH connection

The Proxmox provider can connect to a Proxmox node via SSH. This is used in
the `proxmox_virtual_environment_vm` or `proxmox_virtual_environment_file`
resource to execute commands on the node to perform actions that are not
supported by Proxmox API. For example, to import VM disks, or to uploading
certain type of resources, such as snippets.

The SSH connection configuration is provided via the optional `ssh` block in
the `provider` block:

```terraform
provider "proxmox" {
endpoint = "https://10.0.0.2:8006/"
username = "username@realm"
password = "a-strong-password"
insecure = true
ssh {
agent = true
}
}
```

If no `ssh` block is provided, the provider will attempt to connect to the
target node using the credentials provided in the `username` and `password` fields.
Note that the target node is identified by the `node` argument in the resource,
and may be different from the Proxmox API endpoint. Please refer to the
section below for all the available arguments in the `ssh` block.

## Argument Reference

In addition
Expand All @@ -85,5 +114,20 @@ Proxmox `provider` block:
- `password` - (Required) The password for the Proxmox Virtual Environment
API (can also be sourced from `PROXMOX_VE_PASSWORD`).
- `username` - (Required) The username and realm for the Proxmox Virtual
Environment API (can also be sourced from `PROXMOX_VE_USERNAME`). For
Environment API (can also be sourced from `PROXMOX_VE_USERNAME`). For
example, `root@pam`.
- `ssh` - (Optional) The SSH connection configuration to a Proxmox node. This is
a
block, whose fields are documented below.
- `username` - (Optional) The username to use for the SSH connection.
Defaults to the username used for the Proxmox API connection. Can also be
sourced from `PROXMOX_VE_SSH_USERNAME`.
- `password` - (Optional) The password to use for the SSH connection.
Defaults to the password used for the Proxmox API connection. Can also be
sourced from `PROXMOX_VE_SSH_PASSWORD`.
- `agent` - (Optional) Whether to use the SSH agent for the SSH
authentication. Defaults to `false`. Can also be sourced
from `PROXMOX_VE_SSH_AGENT`.
- `agent_socket` - (Optional) The path to the SSH agent socket.
Defaults to the value of the `SSH_AUTH_SOCK` environment variable. Can
also be sourced from `PROXMOX_VE_SSH_AUTH_SOCK`.
3 changes: 3 additions & 0 deletions example/main.tf
Expand Up @@ -3,4 +3,7 @@ provider "proxmox" {
username = var.virtual_environment_username
password = var.virtual_environment_password
insecure = true
ssh {
agent = true
}
}
46 changes: 37 additions & 9 deletions proxmox/virtual_environment_client.go
@@ -1,6 +1,8 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package proxmox

Expand All @@ -14,6 +16,7 @@ import (
"io"
"net/http"
"net/url"
"runtime"
"strings"

"github.com/google/go-querystring/query"
Expand All @@ -24,7 +27,7 @@ import (
// NewVirtualEnvironmentClient creates and initializes a VirtualEnvironmentClient instance.
func NewVirtualEnvironmentClient(
endpoint, username, password, otp string,
insecure bool,
insecure bool, sshUsername string, sshPassword string, sshAgent bool, sshAgentSocket string,
) (*VirtualEnvironmentClient, error) {
u, err := url.ParseRequestURI(endpoint)
if err != nil {
Expand All @@ -51,6 +54,12 @@ func NewVirtualEnvironmentClient(
)
}

if !strings.Contains(username, "@") {
return nil, errors.New(
"make sure the username for the Proxmox Virtual Environment API ends in '@pve or @pam'",
)
}

var pOTP *string

if otp != "" {
Expand All @@ -68,13 +77,32 @@ func NewVirtualEnvironmentClient(

httpClient := &http.Client{Transport: transport}

if sshUsername == "" {
sshUsername = strings.Split(username, "@")[0]
}

if sshPassword == "" {
sshPassword = password
}

if sshAgent && runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "freebsd" {
return nil, errors.New(
"the ssh agent flag is only supported on POSIX systems, please set it to 'false'" +
" or remove it from your provider configuration",
)
}

return &VirtualEnvironmentClient{
Endpoint: strings.TrimRight(u.String(), "/"),
Insecure: insecure,
OTP: pOTP,
Password: password,
Username: username,
httpClient: httpClient,
Endpoint: strings.TrimRight(u.String(), "/"),
Insecure: insecure,
OTP: pOTP,
Password: password,
Username: username,
SSHUsername: sshUsername,
SSHPassword: sshPassword,
SSHAgent: sshAgent,
SSHAgentSocket: sshAgentSocket,
httpClient: httpClient,
}, nil
}

Expand Down
20 changes: 13 additions & 7 deletions proxmox/virtual_environment_client_types.go
@@ -1,6 +1,8 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package proxmox

Expand All @@ -19,11 +21,15 @@ const (

// VirtualEnvironmentClient implements an API client for the Proxmox Virtual Environment API.
type VirtualEnvironmentClient struct {
Endpoint string
Insecure bool
OTP *string
Password string
Username string
Endpoint string
Insecure bool
OTP *string
Password string
Username string
SSHUsername string
SSHPassword string
SSHAgent bool
SSHAgentSocket string

authenticationData *VirtualEnvironmentAuthenticationResponseData
httpClient *http.Client
Expand Down
68 changes: 61 additions & 7 deletions proxmox/virtual_environment_nodes.go
@@ -1,6 +1,8 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package proxmox

Expand All @@ -21,6 +23,7 @@ import (
"github.com/skeema/knownhosts"

"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)

// ExecuteNodeCommands executes commands on a given node.
Expand Down Expand Up @@ -193,8 +196,6 @@ func (c *VirtualEnvironmentClient) OpenNodeShell(
return nil, err
}

ur := strings.Split(c.Username, "@")

homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to determine the home directory: %w", err)
Expand Down Expand Up @@ -246,8 +247,61 @@ func (c *VirtualEnvironmentClient) OpenNodeShell(
})

sshConfig := &ssh.ClientConfig{
User: ur[0],
Auth: []ssh.AuthMethod{ssh.Password(c.Password)},
User: c.SSHUsername,
Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)},
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}

tflog.Info(ctx, fmt.Sprintf("Agent is set to %t", c.SSHAgent))

if c.SSHAgent {
sshClient, err := c.CreateSSHClientAgent(ctx, cb, kh, sshHost)
if err != nil {
tflog.Error(ctx, "Failed ssh connection through agent, "+
"falling back to password authentication",
map[string]interface{}{
"error": err,
})
} else {
return sshClient, nil
}
}

sshClient, err := ssh.Dial("tcp", sshHost, sshConfig)
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
}

tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
"host": sshHost,
"user": c.SSHUsername,
})
return sshClient, nil
}

// CreateSSHClientAgent establishes an ssh connection through the agent authentication mechanism
func (c *VirtualEnvironmentClient) CreateSSHClientAgent(
ctx context.Context,
cb ssh.HostKeyCallback,
kh knownhosts.HostKeyCallback,
sshHost string,
) (*ssh.Client, error) {
if c.SSHAgentSocket == "" {
return nil, errors.New("failed connecting to SSH agent socket: the socket file is not defined, " +
"authentication will fall back to password")
}

conn, err := net.Dial("unix", c.SSHAgentSocket)
if err != nil {
return nil, fmt.Errorf("failed connecting to SSH auth socket '%s': %w", c.SSHAgentSocket, err)
}

ag := agent.NewClient(conn)

sshConfig := &ssh.ClientConfig{
User: c.SSHUsername,
Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(ag.Signers), ssh.Password(c.SSHPassword)},
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
Expand All @@ -259,7 +313,7 @@ func (c *VirtualEnvironmentClient) OpenNodeShell(

tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
"host": sshHost,
"user": ur[0],
"user": c.SSHUsername,
})
return sshClient, nil
}
Expand Down
34 changes: 29 additions & 5 deletions proxmoxtf/provider/provider.go
Expand Up @@ -17,14 +17,18 @@ import (
)

const (
dvProviderOTP = ""

dvProviderOTP = ""
mkProviderVirtualEnvironment = "virtual_environment"
mkProviderEndpoint = "endpoint"
mkProviderInsecure = "insecure"
mkProviderOTP = "otp"
mkProviderPassword = "password"
mkProviderUsername = "username"
mkProviderSSH = "ssh"
mkProviderSSHUsername = "username"
mkProviderSSHPassword = "password"
mkProviderSSHAgent = "agent"
mkProviderSSHAgentSocket = "agent_socket"
)

// ProxmoxVirtualEnvironment returns the object for this provider.
Expand All @@ -41,26 +45,46 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
var err error
var veClient *proxmox.VirtualEnvironmentClient

// Initialize the client for the Virtual Environment, if required.
// Legacy configuration, wrapped in the deprecated `virtual_environment` block
veConfigBlock := d.Get(mkProviderVirtualEnvironment).([]interface{})

if len(veConfigBlock) > 0 {
veConfig := veConfigBlock[0].(map[string]interface{})
veSSHConfig := veConfig[mkProviderSSH].(map[string]interface{})

veClient, err = proxmox.NewVirtualEnvironmentClient(
veConfig[mkProviderEndpoint].(string),
veConfig[mkProviderUsername].(string),
veConfig[mkProviderSSH].(map[string]interface{})[mkProviderSSHUsername].(string),
veConfig[mkProviderPassword].(string),
veConfig[mkProviderOTP].(string),
veConfig[mkProviderInsecure].(bool),
veSSHConfig[mkProviderSSHUsername].(string),
veSSHConfig[mkProviderSSHPassword].(string),
veSSHConfig[mkProviderSSHAgent].(bool),
veSSHConfig[mkProviderSSHAgentSocket].(string),
)
} else {
sshconf := map[string]interface{}{
mkProviderSSHUsername: "",
mkProviderSSHPassword: "",
mkProviderSSHAgent: false,
mkProviderSSHAgentSocket: "",
}

sshBlock, sshSet := d.GetOk(mkProviderSSH)
if sshSet {
sshconf = sshBlock.(*schema.Set).List()[0].(map[string]interface{})
}

veClient, err = proxmox.NewVirtualEnvironmentClient(
d.Get(mkProviderEndpoint).(string),
d.Get(mkProviderUsername).(string),
d.Get(mkProviderPassword).(string),
d.Get(mkProviderOTP).(string),
d.Get(mkProviderInsecure).(bool),
sshconf[mkProviderSSHUsername].(string),
sshconf[mkProviderSSHPassword].(string),
sshconf[mkProviderSSHAgent].(bool),
sshconf[mkProviderSSHAgentSocket].(string),
)
}

Expand Down

0 comments on commit 9fa9242

Please sign in to comment.