0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-08-22 19:38:35 +00:00

feat(provider): add support for private key authentication for SSH (#1076)

* feat(provider): add support for private key authentication for SSH

Also fix bunch of issues with acceptance tests

---------

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Pavel Boldyrev 2024-03-02 11:10:42 -05:00 committed by GitHub
parent 66ec9f4b9b
commit 2c6d3ad01d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 137 additions and 39 deletions

View File

@ -5,7 +5,6 @@ on:
push:
branches:
- main
- "release/**"
jobs:
build:
@ -30,9 +29,7 @@ jobs:
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
cache-dependency-path: |
go.sum
tools/go.sum
cache-dependency-path: "**/*.sum"
- name: Get dependencies
if: steps.filter.outputs.go == 'true'
@ -63,9 +60,7 @@ jobs:
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
cache-dependency-path: |
go.sum
tools/go.sum
cache-dependency-path: "**/*.sum"
- name: Get dependencies
if: steps.filter.outputs.go == 'true'
@ -88,9 +83,6 @@ jobs:
terraform: [ 1.6 ]
runs-on: ${{ matrix.os }}
environment: pve-acc
concurrency:
group: acceptance
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v4
@ -101,18 +93,11 @@ jobs:
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
cache-dependency-path: |
go.sum
tools/go.sum
cache-dependency-path: "**/*.sum"
- name: Get dependencies
run: go mod download
- name: Setup ssh-agent
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PROXMOX_VE_SSH_USER_PRIVATE_KEY }}
- uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 # v3.0.0
with:
terraform_version: ${{ matrix.terraform }}.*
@ -122,9 +107,11 @@ jobs:
timeout-minutes: 10
env:
TF_ACC: 1
PROXMOX_VE_INSECURE: false
PROXMOX_VE_API_TOKEN: "${{ secrets.PROXMOX_VE_API_TOKEN }}"
PROXMOX_VE_ENDPOINT: "https://${{ secrets.PROXMOX_VE_HOST }}:8006/"
PROXMOX_VE_SSH_AGENT: false
PROXMOX_VE_SSH_USERNAME: "terraform"
PROXMOX_VE_SSH_AGENT: true
PROXMOX_VE_INSECURE: false
PROXMOX_VE_SSH_PRIVATE_KEY: "${{ secrets.PROXMOX_VE_SSH_PRIVATE_KEY }}"
run: make testacc

View File

@ -114,6 +114,41 @@ The provider does not use OS-specific SSH configuration files, such as `~/.ssh/c
Instead, it uses the SSH protocol directly, and supports the `SSH_AUTH_SOCK` environment variable (or `agent_socket` argument) to connect to the `ssh-agent`.
This allows the provider to use the SSH agent configured by the user, and to support multiple SSH agents running on the same machine.
You can find more details on the SSH Agent [here](https://www.digitalocean.com/community/tutorials/ssh-essentials-working-with-ssh-servers-clients-and-keys#adding-your-ssh-keys-to-an-ssh-agent-to-avoid-typing-the-passphrase).
The SSH agent authentication takes precedence over the `private_key` and `password` authentication.
### SSH Private Key
In some cases where SSH agent is not available, for example when running Terraform from a Windows machine, or when using a CI/CD pipeline that does not support SSH agent forwarding,
you can use the `private_key` argument in the `ssh` block (or alternatively `PROXMOX_VE_SSH_PRIVATE_KEY` environment variable) to provide the private key for the SSH connection.
The private key must be in PEM format, and can be loaded from a file:
```terraform
provider "proxmox" {
...
ssh {
agent = false
private_key = file("~/.ssh/id_rsa")
}
}
```
Not recommended, but you can also use a heredoc syntax to provide the private key as a string (note that the private key content must not be indented):
```terraform
provider "proxmox" {
...
ssh {
agent = false
private_key = <<EOF
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
<SKIPPED>
DMUWUEaH7yMCKl7uCZ9xAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----
}
}
```
### SSH User
@ -317,6 +352,7 @@ In addition to [generic provider arguments](https://www.terraform.io/docs/config
- `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`.
- `private_key` - (Optional) The private key to use for the SSH connection. Can also be sourced from `PROXMOX_VE_SSH_PRIVATE_KEY`. The private key must be in PEM format.
- `socks5_server` - (Optional) The address of the SOCKS5 proxy server to use for the SSH connection. Can also be sourced from `PROXMOX_VE_SSH_SOCKS5_SERVER`.
- `socks5_username` - (Optional) The username to use for the SOCKS5 proxy server. Can also be sourced from `PROXMOX_VE_SSH_SOCKS5_USERNAME`.
- `socks5_password` - (Optional) The password to use for the SOCKS5 proxy server. Can also be sourced from `PROXMOX_VE_SSH_SOCKS5_PASSWORD`.

View File

@ -63,6 +63,7 @@ type proxmoxProviderModel struct {
SSH []struct {
Agent types.Bool `tfsdk:"agent"`
AgentSocket types.String `tfsdk:"agent_socket"`
PrivateKey types.String `tfsdk:"private_key"`
Password types.String `tfsdk:"password"`
Username types.String `tfsdk:"username"`
Socks5Server types.String `tfsdk:"socks5_server"`
@ -145,8 +146,9 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"agent": schema.BoolAttribute{
Description: "Whether to use the SSH agent for authentication. " +
"Defaults to `false`.",
Description: "Whether to use the SSH agent for authentication. Takes precedence over " +
"the `private_key` and `password` fields. Defaults to the value of the " +
"`PROXMOX_VE_SSH_AGENT` environment variable, or `false` if not set.",
Optional: true,
},
"agent_socket": schema.StringAttribute{
@ -155,6 +157,12 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
"environment variable.",
Optional: true,
},
"private_key": schema.StringAttribute{
Description: "The unencrypted private key (in PEM format) used for the SSH connection. " +
"Defaults to the value of the `PROXMOX_VE_SSH_PRIVATE_KEY` environment variable.",
Optional: true,
Sensitive: true,
},
"password": schema.StringAttribute{
Description: "The password used for the SSH connection. " +
"Defaults to the value of the `password` field of the " +
@ -332,6 +340,7 @@ func (p *proxmoxProvider) Configure(
sshUsername := utils.GetAnyStringEnv("PROXMOX_VE_SSH_USERNAME")
sshPassword := utils.GetAnyStringEnv("PROXMOX_VE_SSH_PASSWORD")
sshAgent := utils.GetAnyBoolEnv("PROXMOX_VE_SSH_AGENT")
sshPrivateKey := utils.GetAnyStringEnv("PROXMOX_VE_SSH_PRIVATE_KEY")
sshAgentSocket := utils.GetAnyStringEnv("SSH_AUTH_SOCK", "PROXMOX_VE_SSH_AUTH_SOCK")
sshSocks5Server := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_SERVER")
sshSocks5Username := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_USERNAME")
@ -356,6 +365,10 @@ func (p *proxmoxProvider) Configure(
sshAgentSocket = config.SSH[0].AgentSocket.ValueString()
}
if !config.SSH[0].PrivateKey.IsNull() {
sshPrivateKey = config.SSH[0].PrivateKey.ValueString()
}
if !config.SSH[0].Socks5Server.IsNull() {
sshSocks5Server = config.SSH[0].Socks5Server.ValueString()
}
@ -390,7 +403,7 @@ func (p *proxmoxProvider) Configure(
}
sshClient, err := ssh.NewClient(
sshUsername, sshPassword, sshAgent, sshAgentSocket,
sshUsername, sshPassword, sshAgent, sshAgentSocket, sshPrivateKey,
sshSocks5Server, sshSocks5Username, sshSocks5Password,
&apiResolverWithOverrides{
ar: apiResolver{c: apiClient},

View File

@ -9,6 +9,7 @@ package tests
import (
"context"
"fmt"
"math/rand"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@ -21,6 +22,12 @@ const (
accTestContainerCloneName = "proxmox_virtual_environment_container.test_container_clone"
)
//nolint:gochecknoglobals
var (
accTestContainerID = 100000 + rand.Intn(99999) //nolint:gosec
accCloneContainerID = 200000 + rand.Intn(99999) //nolint:gosec
)
func TestAccResourceContainer(t *testing.T) {
accProviders := testAccMuxProviders(context.Background(), t)
@ -50,7 +57,7 @@ resource "proxmox_virtual_environment_download_file" "ubuntu_container_template"
}
resource "proxmox_virtual_environment_container" "test_container" {
node_name = "%s"
vm_id = 1100
vm_id = %d
template = %t
disk {
@ -83,7 +90,7 @@ resource "proxmox_virtual_environment_container" "test_container" {
type = "ubuntu"
}
}
`, accTestNodeName, isTemplate)
`, accTestNodeName, accTestContainerID, isTemplate)
}
func testAccResourceContainerCreateCheck(t *testing.T) resource.TestCheckFunc {
@ -92,8 +99,9 @@ func testAccResourceContainerCreateCheck(t *testing.T) resource.TestCheckFunc {
return resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestContainerName, "description", "my\ndescription\nvalue\n"),
func(*terraform.State) error {
err := getNodesClient().Container(1100).WaitForContainerStatus(context.Background(), "running", 10, 1)
err := getNodesClient().Container(accTestContainerID).WaitForContainerStatus(context.Background(), "running", 10, 1)
require.NoError(t, err, "container did not start")
return nil
},
)
@ -105,17 +113,17 @@ resource "proxmox_virtual_environment_container" "test_container_clone" {
depends_on = [proxmox_virtual_environment_container.test_container]
node_name = "%s"
vm_id = 1101
vm_id = %d
clone {
vm_id = 1100
vm_id = proxmox_virtual_environment_container.test_container.id
}
initialization {
hostname = "test-clone"
}
}
`, accTestNodeName)
`, accTestNodeName, accCloneContainerID)
}
func testAccResourceContainerCreateCloneCheck(t *testing.T) resource.TestCheckFunc {
@ -123,8 +131,9 @@ func testAccResourceContainerCreateCloneCheck(t *testing.T) resource.TestCheckFu
return resource.ComposeTestCheckFunc(
func(*terraform.State) error {
err := getNodesClient().Container(1101).WaitForContainerStatus(context.Background(), "running", 10, 1)
err := getNodesClient().Container(accCloneContainerID).WaitForContainerStatus(context.Background(), "running", 10, 1)
require.NoError(t, err, "container did not start")
return nil
},
)

View File

@ -127,9 +127,9 @@ func uploadSnippetFile(t *testing.T, file *os.File) {
sshUsername := utils.GetAnyStringEnv("PROXMOX_VE_SSH_USERNAME")
sshAgentSocket := utils.GetAnyStringEnv("SSH_AUTH_SOCK", "PROXMOX_VE_SSH_AUTH_SOCK", "PM_VE_SSH_AUTH_SOCK")
sshPrivateKey := utils.GetAnyStringEnv("PROXMOX_VE_SSH_PRIVATE_KEY")
sshClient, err := ssh.NewClient(
sshUsername, "", true, sshAgentSocket,
sshUsername, "", true, sshAgentSocket, sshPrivateKey,
"", "", "",
&nodeResolver{
node: ssh.ProxmoxNode{

View File

@ -58,6 +58,7 @@ type client struct {
password string
agent bool
agentSocket string
privateKey string
socks5Server string
socks5Username string
socks5Password string
@ -68,6 +69,7 @@ type client struct {
func NewClient(
username string, password string,
agent bool, agentSocket string,
privateKey string,
socks5Server string, socks5Username string, socks5Password string,
nodeResolver NodeResolver,
) (Client, error) {
@ -91,6 +93,7 @@ func NewClient(
password: password,
agent: agent,
agentSocket: agentSocket,
privateKey: privateKey,
socks5Server: socks5Server,
socks5Username: socks5Username,
socks5Password: socks5Password,
@ -309,12 +312,26 @@ func (c *client) openNodeShell(ctx context.Context, node ProxmoxNode) (*ssh.Clie
return sshClient, nil
}
tflog.Error(ctx, "Failed ssh connection through agent, falling back to password authentication",
tflog.Error(ctx, "Failed SSH connection through agent",
map[string]interface{}{
"error": err,
})
}
if c.privateKey != "" {
sshClient, err = c.createSSHClientWithPrivateKey(ctx, cb, kh, sshHost)
if err == nil {
return sshClient, nil
}
tflog.Error(ctx, "Failed SSH connection with private key",
map[string]interface{}{
"error": err,
})
}
tflog.Info(ctx, "Falling back to password authentication for SSH connection")
sshClient, err = c.createSSHClient(ctx, cb, kh, sshHost)
if err != nil {
return nil, fmt.Errorf("unable to authenticate user %q over SSH to %q. Please verify that ssh-agent is "+
@ -374,6 +391,27 @@ func (c *client) createSSHClientAgent(
return c.connect(ctx, sshHost, sshConfig)
}
func (c *client) createSSHClientWithPrivateKey(
ctx context.Context,
cb ssh.HostKeyCallback,
kh knownhosts.HostKeyCallback,
sshHost string,
) (*ssh.Client, error) {
privateKey, err := ssh.ParsePrivateKey([]byte(c.privateKey))
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
sshConfig := &ssh.ClientConfig{
User: c.username,
Auth: []ssh.AuthMethod{ssh.PublicKeys(privateKey)},
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
return c.connect(ctx, sshHost, sshConfig)
}
func (c *client) connect(ctx context.Context, sshHost string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
if c.socks5Server != "" {
sshClient, err := c.socks5SSHClient(sshHost, sshConfig)

View File

@ -111,6 +111,7 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
sshPassword := utils.GetAnyStringEnv("PROXMOX_VE_SSH_PASSWORD", "PM_VE_SSH_PASSWORD")
sshAgent := utils.GetAnyBoolEnv("PROXMOX_VE_SSH_AGENT", "PM_VE_SSH_AGENT")
sshAgentSocket := utils.GetAnyStringEnv("SSH_AUTH_SOCK", "PROXMOX_VE_SSH_AUTH_SOCK", "PM_VE_SSH_AUTH_SOCK")
sshPrivateKey := utils.GetAnyStringEnv("PROXMOX_VE_SSH_PRIVATE_KEY")
sshSocks5Server := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_SERVER")
sshSocks5Username := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_USERNAME")
sshSocks5Password := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_PASSWORD")
@ -135,19 +136,23 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
sshConf[mkProviderSSHAgent] = sshAgent
}
if _, ok := sshConf[mkProviderSSHAgentSocket]; !ok {
if v, ok := sshConf[mkProviderSSHAgentSocket]; !ok || v.(string) == "" {
sshConf[mkProviderSSHAgentSocket] = sshAgentSocket
}
if _, ok := sshConf[mkProviderSSHSocks5Server]; !ok {
if v, ok := sshConf[mkProviderSSHPrivateKey]; !ok || v.(string) == "" {
sshConf[mkProviderSSHPrivateKey] = sshPrivateKey
}
if v, ok := sshConf[mkProviderSSHSocks5Server]; !ok || v.(string) == "" {
sshConf[mkProviderSSHSocks5Server] = sshSocks5Server
}
if _, ok := sshConf[mkProviderSSHSocks5Username]; !ok {
if v, ok := sshConf[mkProviderSSHSocks5Username]; !ok || v.(string) == "" {
sshConf[mkProviderSSHSocks5Username] = sshSocks5Username
}
if _, ok := sshConf[mkProviderSSHSocks5Password]; !ok {
if v, ok := sshConf[mkProviderSSHSocks5Password]; !ok || v.(string) == "" {
sshConf[mkProviderSSHSocks5Password] = sshSocks5Password
}
@ -168,6 +173,7 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
sshConf[mkProviderSSHPassword].(string),
sshConf[mkProviderSSHAgent].(bool),
sshConf[mkProviderSSHAgentSocket].(string),
sshConf[mkProviderSSHPrivateKey].(string),
sshConf[mkProviderSSHSocks5Server].(string),
sshConf[mkProviderSSHSocks5Username].(string),
sshConf[mkProviderSSHSocks5Password].(string),

View File

@ -29,6 +29,7 @@ const (
mkProviderSSHPassword = "password"
mkProviderSSHAgent = "agent"
mkProviderSSHAgentSocket = "agent_socket"
mkProviderSSHPrivateKey = "private_key"
mkProviderSSHSocks5Server = "socks5_server"
mkProviderSSHSocks5Username = "socks5_username"
mkProviderSSHSocks5Password = "socks5_password"
@ -123,8 +124,9 @@ func createSchema() map[string]*schema.Schema {
mkProviderSSHAgent: {
Type: schema.TypeBool,
Optional: true,
Description: "Whether to use the SSH agent for authentication. " +
"Defaults to `false`.",
Description: "Whether to use the SSH agent for authentication. Takes precedence over " +
"the `private_key` and `password` fields. Defaults to the value of the " +
"`PROXMOX_VE_SSH_AGENT` environment variable, or `false` if not set.",
DefaultFunc: func() (interface{}, error) {
for _, k := range []string{"PROXMOX_VE_SSH_AGENT", "PM_VE_SSH_AGENT"} {
v := os.Getenv(k)
@ -148,6 +150,13 @@ func createSchema() map[string]*schema.Schema {
),
ValidateFunc: validation.StringIsNotEmpty,
},
mkProviderSSHPrivateKey: {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The unencrypted private key (in PEM format) used for the SSH connection. " +
"Defaults to the value of the `PROXMOX_VE_SSH_PRIVATE_KEY` environment variable.",
},
mkProviderSSHSocks5Server: {
Type: schema.TypeString,
Optional: true,