[GH-ISSUE #153] Parameter verification failed: 'sshkeys' VM config option HTTP query argument is improperly escaped. #80

Closed
opened 2026-02-27 15:46:19 +03:00 by kerem · 5 comments
Owner

Originally created by @aznashwan on GitHub (Jan 11, 2024).
Original GitHub issue: https://github.com/proxmoxer/proxmoxer/issues/153

When updating a VM's config with the sshkeys option, the HTTP query argument is encoded as a query arg using urllib.parse.quote_plus(), which is NOT accepted by the server.

Repro snipped:

proxmox = ProxmoxAPI(...)
vm_client =  proxmox.nodes(YOUR_NODE_ID).qemu(YOUR_VM_ID)

# Contents of `~/.ssh/id_rsa.pub`:
SSH_KEYS = """
ssh-rsa AAAAB3NzaC1yc...
""".strip("\n")

# Expected this to work:
vm_client.config.set(sshkeys=SSH_KEYS)

Error:

Note that, in the error, the key LOOKS like it reached the server correctly, but the error occurs nonetheless.

I suspect the / in my key is what's tripping the server up.

DEBUG:urllib3.connectionpool:https://10.8.1.251:8006 "PUT /api2/json/nodes/proxmox03/qemu/500/config HTTP/1.1" 400 821
Traceback (most recent call last):
  File "/Users/nashwan/Work/projects/coriolis/dir/proxmox/clone_template.py", line 154, in <module>
    update_machine_cloudinit(
  File "/Users/nashwan/Work/projects/coriolis/dir/proxmox/clone_template.py", line 138, in update_machine_cloudinit
    vmcli.config.set(**opts)
  File "/opt/homebrew/lib/python3.11/site-packages/proxmoxer/core.py", line 185, in set
    return self.put(*args, **data)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/proxmoxer/core.py", line 176, in put
    return self(args)._request("PUT", data=data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/proxmoxer/core.py", line 149, in _request
    raise ResourceException(
proxmoxer.core.ResourceException: 400 Bad Request: Parameter verification failed. - {'sshkeys': 'invalid format - invalid urlencoded string: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDwky+OJNqY+fNFyirsAtpxXCrKqDgamMLnXSJeCCotCJq8C8pWMTAFkZVsyFxkMW5xwgvpehmvFhEykysFvW9KdANGCoXzeAyk4+KPxX0T2GHI4etRP2OTqsDGHgjoUD4aiT0eFQXDBlIPRLQB1lCu40dWM44W3Kn3vC6Vmo/so8fIRkog6zdzA4SVBKH/fqbGUkfaOYk7kuKsLIh906RDK9C6kqPs9luQqUAAOMtG8MA4dqknrLQiMVTLWNnp0N2onS4Dobb56mGAd/3jIbEZsav17JYXfJptUNi2Eo+OXbbms5d7w/oFSp3TiGmOO3y96KWc82jcA+X5uX3lchcdDDQURmWiQ9YTq4EQBICgemJp0Qn6uerBnz4w71Ypoa1XLj3sWeya78RyyH2WRf7Vla7yRK3c1z05J0DV1o2CO4AM35l0BlpM8kk5FucwwNdp61qu5wnoISJiqdtiut2h+c5/R7Ln3V3BsHE5olafRaYHH3g1dLDNaoiB74AMg8/w7ZtJemnWcAmYQ/BchH1irHLGOZrSL6rfmyAYuaBa7lOff7Zacb4PGug/L+kWPTimX81s5ZUsMRaoYb8rP0xUMZUFCURA/kp3EG50+ze7+RbInQASYVybIgG3PAlsqrYZtGlX6xGEsZxb0oey5We0LPt797q0ZxpV0mFUe1DAUQ== aznashwan@air\n'}

Debugging request query args:

After turning on debugging at the requests level, it looks like the actual query arg is urllib.parse.quote_plus()'d:

PUT request by Proxmoxer which errors:

# NOTE: the 'sshkeys' arg is encoded using '+' for spaces instead of '%20':
send: b'PUT /api2/json/nodes/proxmox03/qemu/500/config HTTP/1.1\r\nHost: 10.8.1.251:8006\r\nUser-Agent: python-requests/2.31.0\r\nAccept-Encoding: gzip, deflate\r\naccept: application/json, application/x-javascript, text/javascript, text/x-javascript, text/x-json\r\nConnection: keep-alive\r\nCookie: PVEAuthCookie=PVE:root@pam:65A005D9::x9mRIRw3ZHA0fCfvGcTyN4BygJmd/wUwENdKLko6v5SYG7oFjmcY2AoNEkFcKQ7Q/XWc7dPpEg7ivI+/Xpihko7x46MAN9iOO7Z14ewiYpaA2t80ToS2ePRa1/82De48mzwMpee9ebzKVbXuwhS9kOjb2ZmM3XWaa6NydUE9Dpvl+8454AZQIQN1sbhhsKPbmTvc+YnPor1qag2dieVxqTGIvS8We4Dpztc1XQdeUVDjfDinrarLt+Y128NF/eEzYku1PqF5V3KyocRV6JoGIcmnWqtkOlQ+QrR7BZ6SSnnx2x2T8FAivTTYCF/NqTby8IdXrcvsIaCFErXwrJhogQ==\r\nContent-Length: 827\r\nContent-Type: application/x-www-form-urlencoded\r\nCSRFPreventionToken: 65A005D9:+oS/RAN0lYXmfdQDYkjdkE58yKCqbnaWf6KMg3k/uok\r\n\r\n'
send: b'ciuser=coriolis&cipassword=coriolis123&sshkeys=ssh-rsa+AAAAB3NzaC1yc2EAAAADAQABAAACAQDwky%2BOJNqY%2BfNFyirsAtpxXCrKqDgamMLnXSJeCCotCJq8C8pWMTAFkZVsyFxkMW5xwgvpehmvFhEykysFvW9KdANGCoXzeAyk4%2BKPxX0T2GHI4etRP2OTqsDGHgjoUD4aiT0eFQXDBlIPRLQB1lCu40dWM44W3Kn3vC6Vmo%2Fso8fIRkog6zdzA4SVBKH%2FfqbGUkfaOYk7kuKsLIh906RDK9C6kqPs9luQqUAAOMtG8MA4dqknrLQiMVTLWNnp0N2onS4Dobb56mGAd%2F3jIbEZsav17JYXfJptUNi2Eo%2BOXbbms5d7w%2FoFSp3TiGmOO3y96KWc82jcA%2BX5uX3lchcdDDQURmWiQ9YTq4EQBICgemJp0Qn6uerBnz4w71Ypoa1XLj3sWeya78RyyH2WRf7Vla7yRK3c1z05J0DV1o2CO4AM35l0BlpM8kk5FucwwNdp61qu5wnoISJiqdtiut2h%2Bc5%2FR7Ln3V3BsHE5olafRaYHH3g1dLDNaoiB74AMg8%2Fw7ZtJemnWcAmYQ%2FBchH1irHLGOZrSL6rfmyAYuaBa7lOff7Zacb4PGug%2FL%2BkWPTimX81s5ZUsMRaoYb8rP0xUMZUFCURA%2Fkp3EG50%2Bze7%2BRbInQASYVybIgG3PAlsqrYZtGlX6xGEsZxb0oey5We0LPt797q0ZxpV0mFUe1DAUQ%3D%3D+aznashwan%40air'

PUT request as sent from the PVE Web UI (inspected from Safari FWIW)

Request Data
MIME Type: application/x-www-form-urlencoded; charset=UTF-8
# NOTE: the key value is properly encoded using '%20' for spaces.
sshkeys: ssh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAACAQDwky%2BOJNqY%2BfNFyirsAtpxXCrKqDgamMLnXSJeCCotCJq8C8pWMTAFkZVsyFxkMW5xwgvpehmvFhEykysFvW9KdANGCoXzeAyk4%2BKPxX0T2GHI4etRP2OTqsDGHgjoUD4aiT0eFQXDBlIPRLQB1lCu40dWM44W3Kn3vC6Vmo%2Fso8fIRkog6zdzA4SVBKH%2FfqbGUkfaOYk7kuKsLIh906RDK9C6kqPs9luQqUAAOMtG8MA4dqknrLQiMVTLWNnp0N2onS4Dobb56mGAd%2F3jIbEZsav17JYXfJptUNi2Eo%2BOXbbms5d7w%2FoFSp3TiGmOO3y96KWc82jcA%2BX5uX3lchcdDDQURmWiQ9YTq4EQBICgemJp0Qn6uerBnz4w71Ypoa1XLj3sWeya78RyyH2WRf7Vla7yRK3c1z05J0DV1o2CO4AM35l0BlpM8kk5FucwwNdp61qu5wnoISJiqdtiut2h%2Bc5%2FR7Ln3V3BsHE5olafRaYHH3g1dLDNaoiB74AMg8%2Fw7ZtJemnWcAmYQ%2FBchH1irHLGOZrSL6rfmyAYuaBa7lOff7Zacb4PGug%2FL%2BkWPTimX81s5ZUsMRaoYb8rP0xUMZUFCURA%2Fkp3EG50%2Bze7%2BRbInQASYVybIgG3PAlsqrYZtGlX6xGEsZxb0oey5We0LPt797q0ZxpV0mFUe1DAUQ%3D%3D%20aznashwan%40air
digest: 9efc27a8c458b9273a4e82cd6f441c51983e7112

Workaround:

# Simply escape the 'sshkeys' arg yourself:
from urllib import parse as urlparse

# NOTE: by default safe='/' so we have to pass safe='' to clear it.
ENCODED_KEYS = urlparse.quote(SSH_KEYS, safe='')

vm_client.config.set(sshkeys=ENCODED_KEYS)

Conclusion

While I do NOT believe that this warrants hacking core.py just for special treatment of the sshkeys arg, I think it's important for this issue to be documented somewhere.

Originally created by @aznashwan on GitHub (Jan 11, 2024). Original GitHub issue: https://github.com/proxmoxer/proxmoxer/issues/153 When [updating a VM's config](https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/config) with the `sshkeys` option, the HTTP query argument is encoded as a query arg using `urllib.parse.quote_plus()`, which is NOT accepted by the server. ### Repro snipped: ```python3 proxmox = ProxmoxAPI(...) vm_client = proxmox.nodes(YOUR_NODE_ID).qemu(YOUR_VM_ID) # Contents of `~/.ssh/id_rsa.pub`: SSH_KEYS = """ ssh-rsa AAAAB3NzaC1yc... """.strip("\n") # Expected this to work: vm_client.config.set(sshkeys=SSH_KEYS) ``` ### Error: Note that, in the error, the key LOOKS like it reached the server correctly, but the error occurs nonetheless. I suspect the `/` in my key is what's tripping the server up. ```python3 DEBUG:urllib3.connectionpool:https://10.8.1.251:8006 "PUT /api2/json/nodes/proxmox03/qemu/500/config HTTP/1.1" 400 821 Traceback (most recent call last): File "/Users/nashwan/Work/projects/coriolis/dir/proxmox/clone_template.py", line 154, in <module> update_machine_cloudinit( File "/Users/nashwan/Work/projects/coriolis/dir/proxmox/clone_template.py", line 138, in update_machine_cloudinit vmcli.config.set(**opts) File "/opt/homebrew/lib/python3.11/site-packages/proxmoxer/core.py", line 185, in set return self.put(*args, **data) ^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/homebrew/lib/python3.11/site-packages/proxmoxer/core.py", line 176, in put return self(args)._request("PUT", data=data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/homebrew/lib/python3.11/site-packages/proxmoxer/core.py", line 149, in _request raise ResourceException( proxmoxer.core.ResourceException: 400 Bad Request: Parameter verification failed. - {'sshkeys': 'invalid format - invalid urlencoded string: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDwky+OJNqY+fNFyirsAtpxXCrKqDgamMLnXSJeCCotCJq8C8pWMTAFkZVsyFxkMW5xwgvpehmvFhEykysFvW9KdANGCoXzeAyk4+KPxX0T2GHI4etRP2OTqsDGHgjoUD4aiT0eFQXDBlIPRLQB1lCu40dWM44W3Kn3vC6Vmo/so8fIRkog6zdzA4SVBKH/fqbGUkfaOYk7kuKsLIh906RDK9C6kqPs9luQqUAAOMtG8MA4dqknrLQiMVTLWNnp0N2onS4Dobb56mGAd/3jIbEZsav17JYXfJptUNi2Eo+OXbbms5d7w/oFSp3TiGmOO3y96KWc82jcA+X5uX3lchcdDDQURmWiQ9YTq4EQBICgemJp0Qn6uerBnz4w71Ypoa1XLj3sWeya78RyyH2WRf7Vla7yRK3c1z05J0DV1o2CO4AM35l0BlpM8kk5FucwwNdp61qu5wnoISJiqdtiut2h+c5/R7Ln3V3BsHE5olafRaYHH3g1dLDNaoiB74AMg8/w7ZtJemnWcAmYQ/BchH1irHLGOZrSL6rfmyAYuaBa7lOff7Zacb4PGug/L+kWPTimX81s5ZUsMRaoYb8rP0xUMZUFCURA/kp3EG50+ze7+RbInQASYVybIgG3PAlsqrYZtGlX6xGEsZxb0oey5We0LPt797q0ZxpV0mFUe1DAUQ== aznashwan@air\n'} ``` ### Debugging request query args: After turning on debugging at the requests level, it looks like the actual query arg is `urllib.parse.quote_plus()`'d: #### PUT request by Proxmoxer which errors: ```sh # NOTE: the 'sshkeys' arg is encoded using '+' for spaces instead of '%20': send: b'PUT /api2/json/nodes/proxmox03/qemu/500/config HTTP/1.1\r\nHost: 10.8.1.251:8006\r\nUser-Agent: python-requests/2.31.0\r\nAccept-Encoding: gzip, deflate\r\naccept: application/json, application/x-javascript, text/javascript, text/x-javascript, text/x-json\r\nConnection: keep-alive\r\nCookie: PVEAuthCookie=PVE:root@pam:65A005D9::x9mRIRw3ZHA0fCfvGcTyN4BygJmd/wUwENdKLko6v5SYG7oFjmcY2AoNEkFcKQ7Q/XWc7dPpEg7ivI+/Xpihko7x46MAN9iOO7Z14ewiYpaA2t80ToS2ePRa1/82De48mzwMpee9ebzKVbXuwhS9kOjb2ZmM3XWaa6NydUE9Dpvl+8454AZQIQN1sbhhsKPbmTvc+YnPor1qag2dieVxqTGIvS8We4Dpztc1XQdeUVDjfDinrarLt+Y128NF/eEzYku1PqF5V3KyocRV6JoGIcmnWqtkOlQ+QrR7BZ6SSnnx2x2T8FAivTTYCF/NqTby8IdXrcvsIaCFErXwrJhogQ==\r\nContent-Length: 827\r\nContent-Type: application/x-www-form-urlencoded\r\nCSRFPreventionToken: 65A005D9:+oS/RAN0lYXmfdQDYkjdkE58yKCqbnaWf6KMg3k/uok\r\n\r\n' send: b'ciuser=coriolis&cipassword=coriolis123&sshkeys=ssh-rsa+AAAAB3NzaC1yc2EAAAADAQABAAACAQDwky%2BOJNqY%2BfNFyirsAtpxXCrKqDgamMLnXSJeCCotCJq8C8pWMTAFkZVsyFxkMW5xwgvpehmvFhEykysFvW9KdANGCoXzeAyk4%2BKPxX0T2GHI4etRP2OTqsDGHgjoUD4aiT0eFQXDBlIPRLQB1lCu40dWM44W3Kn3vC6Vmo%2Fso8fIRkog6zdzA4SVBKH%2FfqbGUkfaOYk7kuKsLIh906RDK9C6kqPs9luQqUAAOMtG8MA4dqknrLQiMVTLWNnp0N2onS4Dobb56mGAd%2F3jIbEZsav17JYXfJptUNi2Eo%2BOXbbms5d7w%2FoFSp3TiGmOO3y96KWc82jcA%2BX5uX3lchcdDDQURmWiQ9YTq4EQBICgemJp0Qn6uerBnz4w71Ypoa1XLj3sWeya78RyyH2WRf7Vla7yRK3c1z05J0DV1o2CO4AM35l0BlpM8kk5FucwwNdp61qu5wnoISJiqdtiut2h%2Bc5%2FR7Ln3V3BsHE5olafRaYHH3g1dLDNaoiB74AMg8%2Fw7ZtJemnWcAmYQ%2FBchH1irHLGOZrSL6rfmyAYuaBa7lOff7Zacb4PGug%2FL%2BkWPTimX81s5ZUsMRaoYb8rP0xUMZUFCURA%2Fkp3EG50%2Bze7%2BRbInQASYVybIgG3PAlsqrYZtGlX6xGEsZxb0oey5We0LPt797q0ZxpV0mFUe1DAUQ%3D%3D+aznashwan%40air' ``` #### PUT request as sent from the PVE Web UI (inspected from Safari FWIW) ```sh Request Data MIME Type: application/x-www-form-urlencoded; charset=UTF-8 # NOTE: the key value is properly encoded using '%20' for spaces. sshkeys: ssh-rsa%20AAAAB3NzaC1yc2EAAAADAQABAAACAQDwky%2BOJNqY%2BfNFyirsAtpxXCrKqDgamMLnXSJeCCotCJq8C8pWMTAFkZVsyFxkMW5xwgvpehmvFhEykysFvW9KdANGCoXzeAyk4%2BKPxX0T2GHI4etRP2OTqsDGHgjoUD4aiT0eFQXDBlIPRLQB1lCu40dWM44W3Kn3vC6Vmo%2Fso8fIRkog6zdzA4SVBKH%2FfqbGUkfaOYk7kuKsLIh906RDK9C6kqPs9luQqUAAOMtG8MA4dqknrLQiMVTLWNnp0N2onS4Dobb56mGAd%2F3jIbEZsav17JYXfJptUNi2Eo%2BOXbbms5d7w%2FoFSp3TiGmOO3y96KWc82jcA%2BX5uX3lchcdDDQURmWiQ9YTq4EQBICgemJp0Qn6uerBnz4w71Ypoa1XLj3sWeya78RyyH2WRf7Vla7yRK3c1z05J0DV1o2CO4AM35l0BlpM8kk5FucwwNdp61qu5wnoISJiqdtiut2h%2Bc5%2FR7Ln3V3BsHE5olafRaYHH3g1dLDNaoiB74AMg8%2Fw7ZtJemnWcAmYQ%2FBchH1irHLGOZrSL6rfmyAYuaBa7lOff7Zacb4PGug%2FL%2BkWPTimX81s5ZUsMRaoYb8rP0xUMZUFCURA%2Fkp3EG50%2Bze7%2BRbInQASYVybIgG3PAlsqrYZtGlX6xGEsZxb0oey5We0LPt797q0ZxpV0mFUe1DAUQ%3D%3D%20aznashwan%40air digest: 9efc27a8c458b9273a4e82cd6f441c51983e7112 ``` ## Workaround: ```python3 # Simply escape the 'sshkeys' arg yourself: from urllib import parse as urlparse # NOTE: by default safe='/' so we have to pass safe='' to clear it. ENCODED_KEYS = urlparse.quote(SSH_KEYS, safe='') vm_client.config.set(sshkeys=ENCODED_KEYS) ``` ### Conclusion While I do NOT believe that this warrants hacking core.py just for special treatment of the `sshkeys` arg, I think it's important for this issue to be documented somewhere.
kerem closed this issue 2026-02-27 15:46:19 +03:00
Author
Owner

@morph027 commented on GitHub (Jan 11, 2024):

Jip, good catch. Stumbled upon the same some time ago 🙈

https://gitlab.com/morph027/pve-cloud-init-creator/-/blob/master/pve_cloud_init_creator.py?ref_type=heads#L362

<!-- gh-comment-id:1887781932 --> @morph027 commented on GitHub (Jan 11, 2024): Jip, good catch. Stumbled upon the same some time ago :see_no_evil: https://gitlab.com/morph027/pve-cloud-init-creator/-/blob/master/pve_cloud_init_creator.py?ref_type=heads#L362
Author
Owner

@aznashwan commented on GitHub (Jan 12, 2024):

Jip, good catch. Stumbled upon the same some time ago 🙈

Ah sorry to hear, but glad to know I wasn't the first person to ever bang my head against this arg. 😆

I guess a maintainer could mark this issue as "avoidable" and/or close it as they see fit.

Thank you and the rest of the contributors to this nice lib btw!

<!-- gh-comment-id:1888752315 --> @aznashwan commented on GitHub (Jan 12, 2024): > Jip, good catch. Stumbled upon the same some time ago 🙈 Ah sorry to hear, but glad to know I wasn't the first person to ever bang my head against this arg. 😆 I guess a maintainer could mark this issue as "avoidable" and/or close it as they see fit. Thank you and the rest of the contributors to this nice lib btw!
Author
Owner

@jhollowe commented on GitHub (Jan 20, 2024):

If you would be willing, could you add this to the documentation?

<!-- gh-comment-id:1902171102 --> @jhollowe commented on GitHub (Jan 20, 2024): If you would be willing, could you add this to the [documentation](https://github.com/proxmoxer/docs/tree/main/docs/examples)?
Author
Owner

@aznashwan commented on GitHub (Jan 23, 2024):

@jhollowe apologies for the delay, I've been quite busy with other stuff, but finally managed to do a write-up which you can find at: https://github.com/proxmoxer/docs/pull/4

<!-- gh-comment-id:1905752209 --> @aznashwan commented on GitHub (Jan 23, 2024): @jhollowe apologies for the delay, I've been quite busy with other stuff, but finally managed to do a write-up which you can find at: https://github.com/proxmoxer/docs/pull/4
Author
Owner

@Torxed commented on GitHub (Mar 7, 2024):

It would be convenient if proxmoxer could take a list of strings rather than us having to figure out this error, as it's pretty non-descriptive from proxmox. And most issue-post in the forums will claim it's the trailing \n that is the issue (and the error message does contain one after the ssh key). Took a while to get to this issue and to figure out safe='' was the winning strategy.

<!-- gh-comment-id:1984365903 --> @Torxed commented on GitHub (Mar 7, 2024): It would be convenient if proxmoxer could take a list of strings rather than us having to figure out this error, as it's pretty non-descriptive from proxmox. And most issue-post in the forums will claim it's the trailing `\n` that is the issue (and the error message does contain one after the ssh key). Took a while to get to this issue and to figure out `safe=''` was the winning strategy.
Sign in to join this conversation.
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/proxmoxer#80
No description provided.