0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 10:33:46 +00:00

feat: SSH-Agent Support (#306)

* 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>
This commit is contained in:
zoop 2023-05-22 19:34:24 +02:00 committed by GitHub
parent 37494a01b6
commit 9fa92423b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 247 additions and 29 deletions

View File

@ -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
@ -87,3 +116,18 @@ Proxmox `provider` block:
- `username` - (Required) The username and realm for the Proxmox Virtual
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`.

View File

@ -3,4 +3,7 @@ provider "proxmox" {
username = var.virtual_environment_username
password = var.virtual_environment_password
insecure = true
ssh {
agent = true
}
}

View File

@ -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
@ -14,6 +16,7 @@ import (
"io"
"net/http"
"net/url"
"runtime"
"strings"
"github.com/google/go-querystring/query"
@ -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 {
@ -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 != "" {
@ -68,12 +77,31 @@ 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,
SSHUsername: sshUsername,
SSHPassword: sshPassword,
SSHAgent: sshAgent,
SSHAgentSocket: sshAgentSocket,
httpClient: httpClient,
}, nil
}

View File

@ -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
@ -24,6 +26,10 @@ type VirtualEnvironmentClient struct {
OTP *string
Password string
Username string
SSHUsername string
SSHPassword string
SSHAgent bool
SSHAgentSocket string
authenticationData *VirtualEnvironmentAuthenticationResponseData
httpClient *http.Client

View File

@ -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
@ -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.
@ -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)
@ -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),
}
@ -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
}

View File

@ -18,13 +18,17 @@ import (
const (
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.
@ -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),
)
}

View File

@ -98,5 +98,64 @@ func nestedProviderSchema() map[string]*schema.Schema {
},
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderSSH: {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
Description: "The SSH connection configuration to a Proxmox node",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkProviderSSHUsername: {
Type: schema.TypeString,
Optional: true,
Description: fmt.Sprintf("The username used for the SSH connection, "+
"defaults to the user specified in '%s'", mkProviderUsername),
DefaultFunc: schema.MultiEnvDefaultFunc(
[]string{"PROXMOX_VE_SSH_USERNAME", "PM_VE_SSH_USERNAME"},
nil,
),
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderSSHPassword: {
Type: schema.TypeString,
Optional: true,
Description: fmt.Sprintf("The password used for the SSH connection, "+
"defaults to the password specified in '%s'", mkProviderPassword),
DefaultFunc: schema.MultiEnvDefaultFunc(
[]string{"PROXMOX_VE_SSH_PASSWORD", "PM_VE_SSH_PASSWORD"},
nil,
),
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderSSHAgent: {
Type: schema.TypeBool,
Optional: true,
Description: "Whether to use the SSH agent for the SSH authentication. Defaults to false",
DefaultFunc: func() (interface{}, error) {
for _, k := range []string{"PROXMOX_VE_SSH_AGENT", "PM_VE_SSH_AGENT"} {
v := os.Getenv(k)
if v == "true" || v == "1" {
return true, nil
}
}
return false, nil
},
},
mkProviderSSHAgentSocket: {
Type: schema.TypeString,
Optional: true,
Description: "The path to the SSH agent socket. Defaults to the value of the `SSH_AUTH_SOCK` " +
"environment variable",
DefaultFunc: schema.MultiEnvDefaultFunc(
[]string{"SSH_AUTH_SOCK", "PROXMOX_VE_SSH_AUTH_SOCK", "PM_VE_SSH_AUTH_SOCK"},
nil,
),
ValidateFunc: validation.StringIsNotEmpty,
},
},
},
},
}
}