mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 02:31:10 +00:00
feat(provider): add SOCKS5 proxy support for SSH connections (#970)
* feat(provider): add support for SOCKS5 proxy for SSH connection. Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> * fix: linter Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
14836b6c50
commit
da1d7804af
@ -213,9 +213,27 @@ provider "proxmox" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### SSH Connection via SOCKS5 Proxy
|
||||
|
||||
The provider supports SSH connection to the target node via a SOCKS5 proxy.
|
||||
|
||||
To enable the SOCKS5 proxy, you need to configure the `ssh` block in the `provider` block, and specify the `socks5_server` argument:
|
||||
|
||||
```terraform
|
||||
provider "proxmox" {
|
||||
// ...
|
||||
ssh {
|
||||
// ...
|
||||
socks5_server = "ip-or-fqdn-of-socks5-server:port"
|
||||
socks5_username = "username" # optional
|
||||
socks5_password = "password" # optional
|
||||
}
|
||||
}
|
||||
|
||||
If enabled, this method will be used for all SSH connections to the target nodes in the cluster.
|
||||
|
||||
## API Token Authentication
|
||||
|
||||
API Token authentication can be used to authenticate with the Proxmox API without the need to provide a password.
|
||||
@ -296,6 +314,9 @@ 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`.
|
||||
- `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`.
|
||||
- `node` - (Optional) The node configuration for the SSH connection. Can be specified multiple times to provide configuration fo multiple nodes.
|
||||
- `name` - (Required) The name of the node.
|
||||
- `address` - (Required) The FQDN/IP address of the node.
|
||||
|
@ -61,10 +61,13 @@ type proxmoxProviderModel struct {
|
||||
Username types.String `tfsdk:"username"`
|
||||
Password types.String `tfsdk:"password"`
|
||||
SSH []struct {
|
||||
Agent types.Bool `tfsdk:"agent"`
|
||||
AgentSocket types.String `tfsdk:"agent_socket"`
|
||||
Password types.String `tfsdk:"password"`
|
||||
Username types.String `tfsdk:"username"`
|
||||
Agent types.Bool `tfsdk:"agent"`
|
||||
AgentSocket types.String `tfsdk:"agent_socket"`
|
||||
Password types.String `tfsdk:"password"`
|
||||
Username types.String `tfsdk:"username"`
|
||||
Socks5Server types.String `tfsdk:"socks5_server"`
|
||||
Socks5Username types.String `tfsdk:"socks5_username"`
|
||||
Socks5Password types.String `tfsdk:"socks5_password"`
|
||||
|
||||
Nodes []struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
@ -165,6 +168,22 @@ func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, re
|
||||
"`provider` block.",
|
||||
Optional: true,
|
||||
},
|
||||
"socks5_server": schema.StringAttribute{
|
||||
Description: "The address:port of the SOCKS5 proxy server. " +
|
||||
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_SERVER` environment variable.",
|
||||
Optional: true,
|
||||
},
|
||||
"socks5_username": schema.StringAttribute{
|
||||
Description: "The username for the SOCKS5 proxy server. " +
|
||||
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_USERNAME` environment variable.",
|
||||
Optional: true,
|
||||
},
|
||||
"socks5_password": schema.StringAttribute{
|
||||
Description: "The password for the SOCKS5 proxy server. " +
|
||||
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_PASSWORD` environment variable.",
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
Blocks: map[string]schema.Block{
|
||||
"node": schema.ListNestedBlock{
|
||||
@ -314,6 +333,9 @@ func (p *proxmoxProvider) Configure(
|
||||
sshPassword := utils.GetAnyStringEnv("PROXMOX_VE_SSH_PASSWORD")
|
||||
sshAgent := utils.GetAnyBoolEnv("PROXMOX_VE_SSH_AGENT")
|
||||
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")
|
||||
sshSocks5Password := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_PASSWORD")
|
||||
nodeOverrides := map[string]ssh.ProxmoxNode{}
|
||||
|
||||
//nolint: nestif
|
||||
@ -334,6 +356,18 @@ func (p *proxmoxProvider) Configure(
|
||||
sshAgentSocket = config.SSH[0].AgentSocket.ValueString()
|
||||
}
|
||||
|
||||
if !config.SSH[0].Socks5Server.IsNull() {
|
||||
sshSocks5Server = config.SSH[0].Socks5Server.ValueString()
|
||||
}
|
||||
|
||||
if !config.SSH[0].Socks5Username.IsNull() {
|
||||
sshSocks5Username = config.SSH[0].Socks5Username.ValueString()
|
||||
}
|
||||
|
||||
if !config.SSH[0].Socks5Password.IsNull() {
|
||||
sshSocks5Password = config.SSH[0].Socks5Password.ValueString()
|
||||
}
|
||||
|
||||
for _, n := range config.SSH[0].Nodes {
|
||||
nodePort := int32(n.Port.ValueInt64())
|
||||
if nodePort == 0 {
|
||||
@ -357,6 +391,7 @@ func (p *proxmoxProvider) Configure(
|
||||
|
||||
sshClient, err := ssh.NewClient(
|
||||
sshUsername, sshPassword, sshAgent, sshAgentSocket,
|
||||
sshSocks5Server, sshSocks5Username, sshSocks5Password,
|
||||
&apiResolverWithOverrides{
|
||||
ar: apiResolver{c: apiClient},
|
||||
overrides: nodeOverrides,
|
||||
|
@ -129,6 +129,7 @@ func uploadSnippetFile(t *testing.T, file *os.File) {
|
||||
|
||||
sshClient, err := ssh.NewClient(
|
||||
sshUsername, "", true, sshAgentSocket,
|
||||
"", "", "",
|
||||
&nodeResolver{
|
||||
node: ssh.ProxmoxNode{
|
||||
Address: u.Hostname(),
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/skeema/knownhosts"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
"golang.org/x/net/proxy"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/utils"
|
||||
@ -41,17 +42,21 @@ type Client interface {
|
||||
}
|
||||
|
||||
type client struct {
|
||||
username string
|
||||
password string
|
||||
agent bool
|
||||
agentSocket string
|
||||
nodeResolver NodeResolver
|
||||
username string
|
||||
password string
|
||||
agent bool
|
||||
agentSocket string
|
||||
socks5Server string
|
||||
socks5Username string
|
||||
socks5Password string
|
||||
nodeResolver NodeResolver
|
||||
}
|
||||
|
||||
// NewClient creates a new SSH client.
|
||||
func NewClient(
|
||||
username string, password string,
|
||||
agent bool, agentSocket string,
|
||||
socks5Server string, socks5Username string, socks5Password string,
|
||||
nodeResolver NodeResolver,
|
||||
) (Client, error) {
|
||||
if agent && runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "freebsd" {
|
||||
@ -61,16 +66,23 @@ func NewClient(
|
||||
)
|
||||
}
|
||||
|
||||
if (socks5Username != "" || socks5Password != "") && socks5Server == "" {
|
||||
return nil, errors.New("socks5 server is required when socks5 username or password is set")
|
||||
}
|
||||
|
||||
if nodeResolver == nil {
|
||||
return nil, errors.New("node resolver is required")
|
||||
}
|
||||
|
||||
return &client{
|
||||
username: username,
|
||||
password: password,
|
||||
agent: agent,
|
||||
agentSocket: agentSocket,
|
||||
nodeResolver: nodeResolver,
|
||||
username: username,
|
||||
password: password,
|
||||
agent: agent,
|
||||
agentSocket: agentSocket,
|
||||
socks5Server: socks5Server,
|
||||
socks5Username: socks5Username,
|
||||
socks5Password: socks5Password,
|
||||
nodeResolver: nodeResolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -267,6 +279,41 @@ func (c *client) openNodeShell(ctx context.Context, node ProxmoxNode) (*ssh.Clie
|
||||
return kherr
|
||||
})
|
||||
|
||||
tflog.Info(ctx, fmt.Sprintf("agent is set to %t", c.agent))
|
||||
|
||||
var sshClient *ssh.Client
|
||||
if c.agent {
|
||||
sshClient, err = c.createSSHClientAgent(ctx, cb, kh, sshHost)
|
||||
if err == nil {
|
||||
return sshClient, nil
|
||||
}
|
||||
|
||||
tflog.Error(ctx, "Failed ssh connection through agent, falling back to password authentication",
|
||||
map[string]interface{}{
|
||||
"error": err,
|
||||
})
|
||||
}
|
||||
|
||||
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 "+
|
||||
"correctly loaded with an authorized key via 'ssh-add -L' (NOTE: configurations in ~/.ssh/config are "+
|
||||
"not considered by the provider): %w", c.username, sshHost, err)
|
||||
}
|
||||
|
||||
return sshClient, nil
|
||||
}
|
||||
|
||||
func (c *client) createSSHClient(
|
||||
ctx context.Context,
|
||||
cb ssh.HostKeyCallback,
|
||||
kh knownhosts.HostKeyCallback,
|
||||
sshHost string,
|
||||
) (*ssh.Client, error) {
|
||||
if c.password == "" {
|
||||
tflog.Error(ctx, "Using password authentication fallback for SSH connection, but the SSH password is empty")
|
||||
}
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: c.username,
|
||||
Auth: []ssh.AuthMethod{ssh.Password(c.password)},
|
||||
@ -274,39 +321,7 @@ func (c *client) openNodeShell(ctx context.Context, node ProxmoxNode) (*ssh.Clie
|
||||
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
|
||||
}
|
||||
|
||||
tflog.Info(ctx, fmt.Sprintf("agent is set to %t", c.agent))
|
||||
|
||||
var sshClient *ssh.Client
|
||||
if c.agent {
|
||||
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 {
|
||||
if c.password == "" {
|
||||
return nil, fmt.Errorf("unable to authenticate user %q over SSH to %q. Please verify that ssh-agent is "+
|
||||
"correctly loaded with an authorized key via 'ssh-add -L' (NOTE: configurations in ~/.ssh/config are "+
|
||||
"not considered by golang's ssh implementation). The exact error from ssh.Dial: %w", c.username, sshHost, err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
|
||||
}
|
||||
|
||||
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
|
||||
"host": sshHost,
|
||||
"user": c.username,
|
||||
})
|
||||
|
||||
return sshClient, nil
|
||||
return c.connect(ctx, sshHost, sshConfig)
|
||||
}
|
||||
|
||||
// createSSHClientAgent establishes an ssh connection through the agent authentication mechanism.
|
||||
@ -335,6 +350,25 @@ func (c *client) createSSHClientAgent(
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %s via SOCKS5 proxy %s: %w", sshHost, c.socks5Server, err)
|
||||
}
|
||||
|
||||
tflog.Debug(ctx, "SSH connection via SOCKS5 established", map[string]interface{}{
|
||||
"host": sshHost,
|
||||
"socks5_server": c.socks5Server,
|
||||
"user": c.username,
|
||||
})
|
||||
|
||||
return sshClient, nil
|
||||
}
|
||||
|
||||
sshClient, err := ssh.Dial("tcp", sshHost, sshConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
|
||||
@ -347,3 +381,25 @@ func (c *client) createSSHClientAgent(
|
||||
|
||||
return sshClient, nil
|
||||
}
|
||||
|
||||
func (c *client) socks5SSHClient(sshServerAddress string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
|
||||
dialer, err := proxy.SOCKS5("tcp", c.socks5Server, &proxy.Auth{
|
||||
User: c.socks5Username,
|
||||
Password: c.socks5Password,
|
||||
}, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %w", err)
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp", sshServerAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %s via SOCKS5 proxy %s: %w", sshServerAddress, c.socks5Server, err)
|
||||
}
|
||||
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, sshServerAddress, sshConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SSH client connection: %w", err)
|
||||
}
|
||||
|
||||
return ssh.NewClient(sshConn, chans, reqs), nil
|
||||
}
|
||||
|
@ -111,6 +111,9 @@ 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")
|
||||
sshSocks5Server := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_SERVER")
|
||||
sshSocks5Username := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_USERNAME")
|
||||
sshSocks5Password := utils.GetAnyStringEnv("PROXMOX_VE_SSH_SOCKS5_PASSWORD")
|
||||
|
||||
if v, ok := sshConf[mkProviderSSHUsername]; !ok || v.(string) == "" {
|
||||
if sshUsername != "" {
|
||||
@ -136,6 +139,18 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
|
||||
sshConf[mkProviderSSHAgentSocket] = sshAgentSocket
|
||||
}
|
||||
|
||||
if _, ok := sshConf[mkProviderSSHSocks5Server]; !ok {
|
||||
sshConf[mkProviderSSHSocks5Server] = sshSocks5Server
|
||||
}
|
||||
|
||||
if _, ok := sshConf[mkProviderSSHSocks5Username]; !ok {
|
||||
sshConf[mkProviderSSHSocks5Username] = sshSocks5Username
|
||||
}
|
||||
|
||||
if _, ok := sshConf[mkProviderSSHSocks5Password]; !ok {
|
||||
sshConf[mkProviderSSHSocks5Password] = sshSocks5Password
|
||||
}
|
||||
|
||||
nodeOverrides := map[string]ssh.ProxmoxNode{}
|
||||
|
||||
if ns, ok := sshConf[mkProviderSSHNode]; ok {
|
||||
@ -153,6 +168,9 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
|
||||
sshConf[mkProviderSSHPassword].(string),
|
||||
sshConf[mkProviderSSHAgent].(bool),
|
||||
sshConf[mkProviderSSHAgentSocket].(string),
|
||||
sshConf[mkProviderSSHSocks5Server].(string),
|
||||
sshConf[mkProviderSSHSocks5Username].(string),
|
||||
sshConf[mkProviderSSHSocks5Password].(string),
|
||||
&apiResolverWithOverrides{
|
||||
ar: apiResolver{c: apiClient},
|
||||
overrides: nodeOverrides,
|
||||
|
@ -15,20 +15,23 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dvProviderOTP = ""
|
||||
mkProviderEndpoint = "endpoint"
|
||||
mkProviderInsecure = "insecure"
|
||||
mkProviderMinTLS = "min_tls"
|
||||
mkProviderOTP = "otp"
|
||||
mkProviderPassword = "password"
|
||||
mkProviderUsername = "username"
|
||||
mkProviderAPIToken = "api_token"
|
||||
mkProviderTmpDir = "tmp_dir"
|
||||
mkProviderSSH = "ssh"
|
||||
mkProviderSSHUsername = "username"
|
||||
mkProviderSSHPassword = "password"
|
||||
mkProviderSSHAgent = "agent"
|
||||
mkProviderSSHAgentSocket = "agent_socket"
|
||||
dvProviderOTP = ""
|
||||
mkProviderEndpoint = "endpoint"
|
||||
mkProviderInsecure = "insecure"
|
||||
mkProviderMinTLS = "min_tls"
|
||||
mkProviderOTP = "otp"
|
||||
mkProviderPassword = "password"
|
||||
mkProviderUsername = "username"
|
||||
mkProviderAPIToken = "api_token"
|
||||
mkProviderTmpDir = "tmp_dir"
|
||||
mkProviderSSH = "ssh"
|
||||
mkProviderSSHUsername = "username"
|
||||
mkProviderSSHPassword = "password"
|
||||
mkProviderSSHAgent = "agent"
|
||||
mkProviderSSHAgentSocket = "agent_socket"
|
||||
mkProviderSSHSocks5Server = "socks5_server"
|
||||
mkProviderSSHSocks5Username = "socks5_username"
|
||||
mkProviderSSHSocks5Password = "socks5_password"
|
||||
|
||||
mkProviderSSHNode = "node"
|
||||
mkProviderSSHNodeName = "name"
|
||||
@ -145,6 +148,40 @@ func createSchema() map[string]*schema.Schema {
|
||||
),
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
mkProviderSSHSocks5Server: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: "The address:port of the SOCKS5 proxy server. " +
|
||||
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_SERVER` environment variable.",
|
||||
DefaultFunc: schema.MultiEnvDefaultFunc(
|
||||
[]string{"PROXMOX_VE_SSH_SOCKS5_SERVER"},
|
||||
nil,
|
||||
),
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
mkProviderSSHSocks5Username: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: "The username for the SOCKS5 proxy server. " +
|
||||
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_USERNAME` environment variable.",
|
||||
DefaultFunc: schema.MultiEnvDefaultFunc(
|
||||
[]string{"PROXMOX_VE_SSH_SOCKS5_USERNAME"},
|
||||
nil,
|
||||
),
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
mkProviderSSHSocks5Password: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
Description: "The password for the SOCKS5 proxy server. " +
|
||||
"Defaults to the value of the `PROXMOX_VE_SSH_SOCKS5_PASSWORD` environment variable.",
|
||||
DefaultFunc: schema.MultiEnvDefaultFunc(
|
||||
[]string{"PROXMOX_VE_SSH_SOCKS5_PASSWORD"},
|
||||
nil,
|
||||
),
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
mkProviderSSHNode: {
|
||||
Type: schema.TypeList,
|
||||
Optional: true,
|
||||
|
Loading…
Reference in New Issue
Block a user