Skip to content

Commit d9834a3

Browse files
authored
fix: respect SSH key options for the root user (#6585)
When no user is provided in the datasource and root login is not disabled, the root user is configured. Preserve the provided SSH key options from user-data for root user in this case. Before this fix, SSH key options passed in the key line were silently ignored. This is what happens: * `apply_credentials` passes `options=""` to `setup_user_keys`: https://github.com/canonical/cloud-init/blob/25.3/cloudinit/config/cc_ssh.py#L257 * `setup_user_keys` receives the empty parameter and passes it to the `parse` function: https://github.com/canonical/cloud-init/blob/25.3/cloudinit/ssh_util.py#L463 * The `parse` function decides whether options from the key line need to be overridden by the `options` parameter. Treat any falsy `options` parameter should be treated like `None`, otherwise options from the key line are ignored. Fixes: GH-3868
1 parent 0686791 commit d9834a3

File tree

3 files changed

+67
-1
lines changed

3 files changed

+67
-1
lines changed

cloudinit/ssh_util.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ def parse_ssh_key(ent):
166166
(keytype, base64, comment) = parse_ssh_key(ent)
167167
except TypeError:
168168
(keyopts, remain) = self._extract_options(ent)
169-
if options is None:
169+
# If the options parameter is falsy, use options from the key line
170+
# (no override)
171+
if not options:
170172
options = keyopts
171173

172174
try:

tests/integration_tests/modules/test_ssh_keys_provided.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
``tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml''``.)"""
99

1010
import pytest
11+
import yaml
1112

1213
from tests.integration_tests.releases import CURRENT_RELEASE
1314

@@ -16,6 +17,7 @@
1617
disable_root: false
1718
ssh_authorized_keys:
1819
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXW9Gg5H7ehjdSc6qDzwNtgCy94XYHhEYlXZMO2+FJrH3wfHGiMfCwOHxcOMt2QiXItULthdeQWS9QjBSSjVRXf6731igFrqPFyS9qBlOQ5D29C4HBXFnQggGVpBNJ82IRJv7szbbe/vpgLBP4kttUza9Dr4e1YM1ln4PRnjfXea6T0m+m1ixNb5432pTXlqYOnNOxSIm1gHgMLxPuDrJvQERDKrSiKSjIdyC9Jd8t2e1tkNLY0stmckVRbhShmcJvlyofHWbc2Ca1mmtP7MlS1VQnfLkvU1IrFwkmaQmaggX6WR6coRJ6XFXdWcq/AI2K6GjSnl1dnnCxE8VCEXBlXgFzad+PMSG4yiL5j8Oo1ZVpkTdgBnw4okGqTYCXyZg6X00As9IBNQfZMFlQXlIo4FiWgj3CO5QHQOyOX6FuEumaU13GnERrSSdp9tCs1Qm3/DG2RSCQBWTfcgMcStIvKqvJ3IjFn0vGLvI3Ampnq9q1SHwmmzAPSdzcMA76HyMUA5VWaBvWHlUxzIM6unxZASnwvuCzpywSEB5J2OF+p6H+cStJwQ32XwmOG8pLp1srlVWpqZI58Du/lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs506oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w==
20+
- no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA1J77+CrJ8p6/vWCEzuylqJNMHUP/XmeYyGVWb8lnDd root@test-with-options
1921
ssh_keys:
2022
rsa_private: |
2123
-----BEGIN RSA PRIVATE KEY-----
@@ -126,3 +128,15 @@ def test_sshd_config(self, class_client):
126128
sshd_config = class_client.read_from_file(sshd_config_path).strip()
127129
for expected_cert in expected_certs:
128130
assert expected_cert in sshd_config
131+
132+
def test_authorized_keys_with_options(self, class_client):
133+
"""Test that SSH authorized key with options is properly persisted."""
134+
authorized_keys = (
135+
class_client.read_from_file("/root/.ssh/authorized_keys")
136+
.strip()
137+
.splitlines()
138+
)
139+
140+
# Check that the keys with SSH options are present and enabled
141+
for userdata_key in yaml.safe_load(USER_DATA)["ssh_authorized_keys"]:
142+
assert userdata_key in authorized_keys

tests/unittests/test_ssh_util.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,56 @@ def test_parse(self, ktype, with_comment, with_options):
314314
else:
315315
assert key.comment == ""
316316

317+
@pytest.mark.parametrize("with_options", [True, False])
318+
@pytest.mark.parametrize("with_comment", [True, False])
319+
@pytest.mark.parametrize("options_parameter", [None, "", 'from="::1"'])
320+
@pytest.mark.parametrize("ktype", ["rsa", "ed25519"])
321+
def test_parse_with_options_parameter(
322+
self, ktype, with_comment, with_options, options_parameter
323+
):
324+
"""Test options parameter behavior with different values.
325+
326+
This test specifically validates that the options parameter passed to
327+
parse() correctly overrides or is overridden by options in the key
328+
line.
329+
It is very similar to test_parse() but only runs for 2 key types to
330+
avoid having too many parameterized tests.
331+
"""
332+
content = VALID_CONTENT[ktype]
333+
comment = "user-%s@host" % ktype
334+
335+
line_args = []
336+
if with_options:
337+
line_args.append(TEST_OPTIONS)
338+
line_args.extend(
339+
[
340+
ktype,
341+
content,
342+
]
343+
)
344+
if with_comment:
345+
line_args.append(comment)
346+
line = " ".join(line_args)
347+
348+
key = ssh_util.AuthKeyLineParser().parse(
349+
line, options=options_parameter
350+
)
351+
352+
assert key.base64 == content
353+
assert key.keytype == ktype
354+
if options_parameter:
355+
# When the options parameter is truthy, override options from line
356+
assert key.options == options_parameter
357+
elif with_options:
358+
# When the options parameter is falsy and line has options,
359+
# those are returned
360+
assert key.options == TEST_OPTIONS
361+
else:
362+
# When the options parameter is falsy and the line has no options,
363+
# the same falsy value is returned (e.g. None or "")
364+
assert key.options == options_parameter
365+
assert key.comment == (comment if with_comment else "")
366+
317367
def test_parse_with_options_passed_in(self):
318368
# test key line with key type and base64 only
319369
parser = ssh_util.AuthKeyLineParser()

0 commit comments

Comments
 (0)