[GH-ISSUE #144] Weird "sshkeys" SSH public key validation error #33

Open
opened 2026-03-03 15:29:45 +03:00 by kerem · 13 comments
Owner

Originally created by @justinclift on GitHub (Apr 29, 2024).
Original GitHub issue: https://github.com/luthermonson/go-proxmox/issues/144

I'm trying to create a VM with a cloud-init ssh key, but the server keeps on returning 500 SSH public key validation error. 😕

It seems pretty weird, as the exact same key works fine when using qm on the server itself to create the VM.

The code in question for creating the VM:

vmProps := []proxmox.VirtualMachineOption{
	{Name: "name", Value: "test1"},
	{Name: "memory", Value: 1024 * 8},
	{Name: "cores", Value: 6},
	{Name: "cpu", Value: "host"},
	{Name: "net0", Value: "model=virtio,bridge=" + publicBridge + ",firewall=1"},
	{Name: "scsihw", Value: "virtio-scsi-single"},
	{Name: "virtio1", Value: "local-zfs:16,cache=" + cacheMode + ",discard=on,iothread=1"},
	{Name: "agent", Value: 1},
	{Name: "ostype", Value: "l26"},
	{Name: "localtime", Value: "0"},
	{Name: "ide0", Value: "local-zfs:cloudinit"},
	{Name: "ipconfig0", Value: "gw=10.1.1.1,ip=10.1.248." + strconv.Itoa(vmID) + "/16"},
	{Name: "sshkeys", Value: url.QueryEscape("/root/.ssh/id_rsa.pub")},
}
vmTask, err := node.NewVirtualMachine(ctx, vmID, vmProps...)
if err != nil {
	log.Fatal(err)
}

If I remove that last vmProps line (the sshkeys one), then the vm creation works. With that in place though, I'm getting:

2024/04/30 04:12:44 500 SSH public key validation error

The working qm version of it:

# Create a new VM
qm create ${VMID} --name "test1" \
  --cpu host \
  --cores 6 \
  --memory 8192 \
  --net0 virtio,bridge=${PUBLIC_BRIDGE},firewall=1 \
  --scsihw virtio-scsi-single \
  --virtio1 local-zfs:16,cache=${CACHE_MODE},discard=on,iothread=1 \
  --agent 1 \
  --ostype l26 \
  --localtime 0 \
  --ide0 local-zfs:cloudinit \
  --ipconfig0 gw=10.1.1.1,ip=10.1.248.${VMID}/16 \
  --sshkeys "~/.ssh/id_rsa.pub"

Anyone have ideas what could be going wrong?

Originally created by @justinclift on GitHub (Apr 29, 2024). Original GitHub issue: https://github.com/luthermonson/go-proxmox/issues/144 I'm trying to create a VM with a cloud-init ssh key, but the server keeps on returning `500 SSH public key validation error`. :confused: It seems pretty weird, as the exact same key works fine when using `qm` on the server itself to create the VM. The code in question for creating the VM: ``` vmProps := []proxmox.VirtualMachineOption{ {Name: "name", Value: "test1"}, {Name: "memory", Value: 1024 * 8}, {Name: "cores", Value: 6}, {Name: "cpu", Value: "host"}, {Name: "net0", Value: "model=virtio,bridge=" + publicBridge + ",firewall=1"}, {Name: "scsihw", Value: "virtio-scsi-single"}, {Name: "virtio1", Value: "local-zfs:16,cache=" + cacheMode + ",discard=on,iothread=1"}, {Name: "agent", Value: 1}, {Name: "ostype", Value: "l26"}, {Name: "localtime", Value: "0"}, {Name: "ide0", Value: "local-zfs:cloudinit"}, {Name: "ipconfig0", Value: "gw=10.1.1.1,ip=10.1.248." + strconv.Itoa(vmID) + "/16"}, {Name: "sshkeys", Value: url.QueryEscape("/root/.ssh/id_rsa.pub")}, } vmTask, err := node.NewVirtualMachine(ctx, vmID, vmProps...) if err != nil { log.Fatal(err) } ``` If I remove that last vmProps line (the `sshkeys` one), then the vm creation works. With that in place though, I'm getting: ``` 2024/04/30 04:12:44 500 SSH public key validation error ``` The working `qm` version of it: ``` # Create a new VM qm create ${VMID} --name "test1" \ --cpu host \ --cores 6 \ --memory 8192 \ --net0 virtio,bridge=${PUBLIC_BRIDGE},firewall=1 \ --scsihw virtio-scsi-single \ --virtio1 local-zfs:16,cache=${CACHE_MODE},discard=on,iothread=1 \ --agent 1 \ --ostype l26 \ --localtime 0 \ --ide0 local-zfs:cloudinit \ --ipconfig0 gw=10.1.1.1,ip=10.1.248.${VMID}/16 \ --sshkeys "~/.ssh/id_rsa.pub" ``` Anyone have ideas what could be going wrong?
Author
Owner

@justinclift commented on GitHub (Apr 29, 2024):

Not sure if it's relevant, but the same error was showing up in a fork of Proxmox at one point: https://github.com/pimox/pimox7/issues/45

<!-- gh-comment-id:2083387655 --> @justinclift commented on GitHub (Apr 29, 2024): Not sure if it's relevant, but the same error was showing up in a fork of Proxmox at one point: https://github.com/pimox/pimox7/issues/45
Author
Owner

@justinclift commented on GitHub (Apr 29, 2024):

Interestingly, the above mentioned error in the other fork seems relevant here too.

If I comment out that same die "SSH public key validation error\n" if $@; line (line 1726 with modern Proxmox), then the ssh key is accepted and things seem happy:

# vi /usr/share/perl5/PVE/Tools.pm
# systemctl restart pvedaemon   <-- so the edited Tools.pm gets loaded

Although the ssh key is accepted, and shows up in the cloud-init section for the VM, it's not actually happy. The key itself doesn't appear to have been loaded into the user in question.

Checked by looking through the VM disk with a rescue system, and the authorized_keys file it should have been loaded into is 0 bytes.

So, maybe something in the Go code really is mucking up that string somehow.

<!-- gh-comment-id:2083489366 --> @justinclift commented on GitHub (Apr 29, 2024): Interestingly, the above mentioned error in the other fork seems relevant here too. If I comment out that same `die "SSH public key validation error\n" if $@;` line (line 1726 with modern Proxmox), then the ssh key is accepted ~and things seem happy~: ``` # vi /usr/share/perl5/PVE/Tools.pm # systemctl restart pvedaemon <-- so the edited Tools.pm gets loaded ``` --- Although the ssh key is accepted, and shows up in the cloud-init section for the VM, it's not actually happy. The key itself doesn't appear to have been loaded into the user in question. Checked by looking through the VM disk with a rescue system, and the `authorized_keys` file it should have been loaded into is 0 bytes. So, maybe something in the Go code really is mucking up that string somehow.
Author
Owner

@justinclift commented on GitHub (Apr 29, 2024):

Hmmm, the server might actually be wanting the actual text of the ssh key, rather than a path.

However, I'm not having much luck in figuring out what PVE calls "url encoding" as it doesn't seem to be any of the common URL encoding calls in Go. 😦

Example:

{Name: "sshkeys", Value: url.PathEscape("ssh-rsa AAAAB ...")}

Result:

2024/04/30 06:34:41 bad request: 400 Parameter verification failed. - {"sshkeys":"invalid format - invalid urlencoded string: ssh-rsa%20AAAAB ...

Also tried base64.URLEncoding.EncodeToString(), base64.RawURLEncoding.EncodeToString(), base64.StdEncoding.EncodeToString() and url.QueryEscape() without any improvement.

It feels like PVE may have it's own ideas about url encoding, and there might need to be a special purpose encoder created just for this one interaction with it. 😉

<!-- gh-comment-id:2083635383 --> @justinclift commented on GitHub (Apr 29, 2024): Hmmm, the server might actually be wanting the actual text of the ssh key, rather than a path. However, I'm not having much luck in figuring out what PVE calls "url encoding" as it doesn't seem to be any of the common URL encoding calls in Go. :frowning: Example: ``` {Name: "sshkeys", Value: url.PathEscape("ssh-rsa AAAAB ...")} ``` Result: ``` 2024/04/30 06:34:41 bad request: 400 Parameter verification failed. - {"sshkeys":"invalid format - invalid urlencoded string: ssh-rsa%20AAAAB ... ``` Also tried `base64.URLEncoding.EncodeToString()`, `base64.RawURLEncoding.EncodeToString()`, `base64.StdEncoding.EncodeToString()` and `url.QueryEscape()` without any improvement. It feels like PVE may have it's own ideas about url encoding, and there might need to be a special purpose encoder created just for this one interaction with it. :wink:
Author
Owner

@justinclift commented on GitHub (Apr 29, 2024):

Ahhh yep, got it somewhat figured out. That sshkeys does need the whole ssh key as the value, and the problem does seem to be in how it's presently getting quoted.

Someone on the Proxmox forums had a similar issue (not via Go though) a Proxmox staff member gave a Python solution for doing the quoting:

https://forum.proxmox.com/threads/how-to-use-pvesh-set-vms-sshkeys.52570/#post-243381

urllib.quote(key, safe='')

That's a Python 2 version of things. As Python 3 is where things are at these days, it's now:

$ python3
>>> import urllib.parse
>>> urllib.parse.quote(key, safe='')

Using that Python 3 snippet I was able to manually url encode an ssh key such that Proxmox accepts it:

$ python3
>>> import urllib.parse
>>> urllib.parse.quote("ssh-rsa AAAAB[...]", safe='')
'ssh-rsa%20AAAAB[...]

Copying that string into the Go vmProps and just passing it directly works, with the VM being created and the ssh key name showing up in the webUI the same way it does for qm:

{Name: "sshkeys", Value: "ssh-rsa%20AAAAB[...]}
<!-- gh-comment-id:2083703789 --> @justinclift commented on GitHub (Apr 29, 2024): Ahhh yep, got it somewhat figured out. That `sshkeys` does need the whole ssh key as the value, and the problem does seem to be in how it's presently getting quoted. Someone on the Proxmox forums had a similar issue (not via Go though) a Proxmox staff member gave a Python solution for doing the quoting: https://forum.proxmox.com/threads/how-to-use-pvesh-set-vms-sshkeys.52570/#post-243381 ``` urllib.quote(key, safe='') ``` That's a Python 2 version of things. As Python 3 is where things are at these days, it's now: ``` $ python3 >>> import urllib.parse >>> urllib.parse.quote(key, safe='') ``` Using that Python 3 snippet I was able to manually url encode an ssh key such that Proxmox accepts it: ``` $ python3 >>> import urllib.parse >>> urllib.parse.quote("ssh-rsa AAAAB[...]", safe='') 'ssh-rsa%20AAAAB[...] ``` Copying that string into the Go vmProps and just passing it directly works, with the VM being created and the ssh key name showing up in the webUI the same way it does for `qm`: ``` {Name: "sshkeys", Value: "ssh-rsa%20AAAAB[...]} ```
Author
Owner

@luthermonson commented on GitHub (Apr 29, 2024):

yup and the docs say you can do multilple keys in one config just separate with a newline. glad you figured it out!

<!-- gh-comment-id:2083719092 --> @luthermonson commented on GitHub (Apr 29, 2024): yup and the docs say you can do multilple keys in one config just separate with a newline. glad you figured it out!
Author
Owner

@justinclift commented on GitHub (Apr 29, 2024):

Any idea if there's a Go function call for doing the "url encoding" that Proxmox wants?

<!-- gh-comment-id:2083723741 --> @justinclift commented on GitHub (Apr 29, 2024): Any idea if there's a Go function call for doing the "url encoding" that Proxmox wants?
Author
Owner

@luthermonson commented on GitHub (Apr 29, 2024):

try this: https://pkg.go.dev/net/url#QueryEscape

<!-- gh-comment-id:2083725362 --> @luthermonson commented on GitHub (Apr 29, 2024): try this: https://pkg.go.dev/net/url#QueryEscape
Author
Owner

@justinclift commented on GitHub (Apr 29, 2024):

Heh, that's literally one of my above examples of something that doesn't work. 😉

Tried it again now, just in case... and nope, it's definitely not a winner:

 {"sshkeys":"invalid format - invalid urlencoded string: ssh-rsa+AAAAB (etc)

That's from calling it this way in my Go code:

{Name: "sshkeys", Value: url.QueryEscape("ssh-rsa AAAAB (etc)
<!-- gh-comment-id:2083731022 --> @justinclift commented on GitHub (Apr 29, 2024): Heh, that's literally one of my above examples of something that doesn't work. :wink: Tried it again now, just in case... and nope, it's definitely not a winner: ``` {"sshkeys":"invalid format - invalid urlencoded string: ssh-rsa+AAAAB (etc) ``` That's from calling it this way in my Go code: ``` {Name: "sshkeys", Value: url.QueryEscape("ssh-rsa AAAAB (etc) ```
Author
Owner

@luthermonson commented on GitHub (Apr 30, 2024):

github.com/proxmox/pve-common@1a6005ad23/src/PVE/JSONSchema.pm (L187)

it appears to be failing this regex, check your output from your funcs to escape and find the right combintation

<!-- gh-comment-id:2083906815 --> @luthermonson commented on GitHub (Apr 30, 2024): https://github.com/proxmox/pve-common/blob/1a6005ad2377b6586e084b3840ac622752b666b8/src/PVE/JSONSchema.pm#L187 it appears to be failing this regex, check your output from your funcs to escape and find the right combintation
Author
Owner

@justinclift commented on GitHub (Apr 30, 2024):

Thanks. Saw that and might investigate that later on. 😄

<!-- gh-comment-id:2084288481 --> @justinclift commented on GitHub (Apr 30, 2024): Thanks. Saw that and might investigate that later on. :smile:
Author
Owner

@LewsTherinSedai commented on GitHub (Jun 29, 2024):

Facing this same issue - if Proxmox is going to even come close to VMWare on an enterprise level, or even SMB, this kind of stuff can't exist. How has no one fixed this blatant UI issue - I have tried multiple forms of SSH keys generated from PuttyGen (including removing the Comment line as documentation says it isn't supported) and I cannot get this to work.

I'm not going to go editing my Proxmox - this should work out of the box - or at a minimum there should be a clear guide on how it should work (i.e. if the PuttyGen style SSH key doesn't work, then what does.)

<!-- gh-comment-id:2198249954 --> @LewsTherinSedai commented on GitHub (Jun 29, 2024): Facing this same issue - if Proxmox is going to even come close to VMWare on an enterprise level, or even SMB, this kind of stuff can't exist. How has no one fixed this blatant UI issue - I have tried multiple forms of SSH keys generated from PuttyGen (including removing the Comment line as documentation says it isn't supported) and I cannot get this to work. I'm not going to go editing my Proxmox - this should work out of the box - or at a minimum there should be a clear guide on how it should work (i.e. if the PuttyGen style SSH key doesn't work, then what does.)
Author
Owner

@justinclift commented on GitHub (Jun 29, 2024):

Ahhh. Sorry for not investigating this further. After finding the lack of disk import functionality (#145) I've given up on using the Proxmox API until basic required functionality (aka "being able to create a new VM") is present.

<!-- gh-comment-id:2198258030 --> @justinclift commented on GitHub (Jun 29, 2024): Ahhh. Sorry for not investigating this further. After finding the lack of disk import functionality (#145) I've given up on using the Proxmox API until basic required functionality (aka "being able to create a new VM") is present.
Author
Owner

@abbaszai commented on GitHub (Aug 14, 2025):

btw, this is how I got it to work in Go:

func formatSSHKeys(rawKeys []string) string {
	cleanedKeys := make([]string, 0, len(rawKeys))

	for _, key := range rawKeys {
		// Handle both encoded and unencoded keys gracefully
		// Try to URL decode in case the key is pre-encoded, but don't fail if it's not
		decodedKey, err := url.QueryUnescape(key)
		if err != nil {
			// If decoding fails, the key is probably already in normal format
			decodedKey = key
		}

		// Remove any trailing newlines and whitespace from each key
		cleanedKey := strings.TrimSpace(decodedKey)
		if cleanedKey != "" {
			cleanedKeys = append(cleanedKeys, cleanedKey)
		}
	}

	// For single keys, don't add newlines. For multiple keys, join with newlines
	var result string
	if len(cleanedKeys) == 1 {
		result = cleanedKeys[0]
	} else {
		result = strings.Join(cleanedKeys, "\n")
	}

	// URL encode using the exact equivalent of Python's urllib.parse.quote(string, safe='')
	// This encodes ALL characters that need encoding, including @ symbols
	encodedResult := url.QueryEscape(result)
	// Convert + back to %20 (QueryEscape uses + for spaces, but Proxmox expects %20)
	encodedResult = strings.ReplaceAll(encodedResult, "+", "%20")

	// Double-check for any trailing newlines in the encoded result and remove them
	finalResult := strings.TrimSuffix(encodedResult, "%0A") // Remove URL-encoded newline
	finalResult = strings.TrimSuffix(finalResult, "\n")     // Remove literal newline

	return finalResult
}
<!-- gh-comment-id:3189002211 --> @abbaszai commented on GitHub (Aug 14, 2025): btw, this is how I got it to work in Go: ``` func formatSSHKeys(rawKeys []string) string { cleanedKeys := make([]string, 0, len(rawKeys)) for _, key := range rawKeys { // Handle both encoded and unencoded keys gracefully // Try to URL decode in case the key is pre-encoded, but don't fail if it's not decodedKey, err := url.QueryUnescape(key) if err != nil { // If decoding fails, the key is probably already in normal format decodedKey = key } // Remove any trailing newlines and whitespace from each key cleanedKey := strings.TrimSpace(decodedKey) if cleanedKey != "" { cleanedKeys = append(cleanedKeys, cleanedKey) } } // For single keys, don't add newlines. For multiple keys, join with newlines var result string if len(cleanedKeys) == 1 { result = cleanedKeys[0] } else { result = strings.Join(cleanedKeys, "\n") } // URL encode using the exact equivalent of Python's urllib.parse.quote(string, safe='') // This encodes ALL characters that need encoding, including @ symbols encodedResult := url.QueryEscape(result) // Convert + back to %20 (QueryEscape uses + for spaces, but Proxmox expects %20) encodedResult = strings.ReplaceAll(encodedResult, "+", "%20") // Double-check for any trailing newlines in the encoded result and remove them finalResult := strings.TrimSuffix(encodedResult, "%0A") // Remove URL-encoded newline finalResult = strings.TrimSuffix(finalResult, "\n") // Remove literal newline return finalResult } ```
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/go-proxmox#33
No description provided.