summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Lung <lungj@users.noreply.github.com>2020-11-20 15:59:51 -0500
committerGitHub <noreply@github.com>2020-11-20 13:59:51 -0700
commit66b4be8b6da188a0667bd8c86a25155b6f4f3f6c (patch)
treec9fff98d09c817586acfb432665d9779debc5b7f
parent5d4a9a4a50a496d27510f63217bcc0c25d9a8939 (diff)
downloadcloud-init-git-66b4be8b6da188a0667bd8c86a25155b6f4f3f6c.tar.gz
Support configuring SSH host certificates. (#660)
Existing config writes keys to /etc/ssh after deleting files matching a glob that includes certificate files. Since sshd looks for certificates in the same directory as the keys, a host certificate must be placed in this directory. This update enables the certificate's contents to be specified along with the keys. Co-authored-by: jonathan lung <lungj@heresjono.com> Co-authored-by: jonathan lung <jlung@kepler.space>
-rwxr-xr-xcloudinit/config/cc_ssh.py31
-rw-r--r--cloudinit/config/tests/test_ssh.py68
-rw-r--r--tests/integration_tests/modules/test_ssh_keys_provided.py13
-rw-r--r--tools/.github-cla-signers1
4 files changed, 101 insertions, 12 deletions
diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
index 9b2a333a..05a16dbc 100755
--- a/cloudinit/config/cc_ssh.py
+++ b/cloudinit/config/cc_ssh.py
@@ -83,8 +83,9 @@ enabled by default.
Host keys can be added using the ``ssh_keys`` configuration key. The argument
to this config key should be a dictionary entries for the public and private
keys of each desired key type. Entries in the ``ssh_keys`` config dict should
-have keys in the format ``<key type>_private`` and ``<key type>_public``,
-e.g. ``rsa_private: <key>`` and ``rsa_public: <key>``. See below for supported
+have keys in the format ``<key type>_private``, ``<key type>_public``, and,
+optionally, ``<key type>_certificate``, e.g. ``rsa_private: <key>``,
+``rsa_public: <key>``, and ``rsa_certificate: <key>``. See below for supported
key types. Not all key types have to be specified, ones left unspecified will
not be used. If this config option is used, then no keys will be generated.
@@ -94,7 +95,8 @@ not be used. If this config option is used, then no keys will be generated.
secure
.. note::
- to specify multiline private host keys, use yaml multiline syntax
+ to specify multiline private host keys and certificates, use yaml
+ multiline syntax
If no host keys are specified using ``ssh_keys``, then keys will be generated
using ``ssh-keygen``. By default one public/private pair of each supported
@@ -128,12 +130,17 @@ config flags are:
...
-----END RSA PRIVATE KEY-----
rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ...
+ rsa_certificate: |
+ ssh-rsa-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQt ...
dsa_private: |
-----BEGIN DSA PRIVATE KEY-----
MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco
...
-----END DSA PRIVATE KEY-----
dsa_public: ssh-dsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ...
+ dsa_certificate: |
+ ssh-dsa-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQt ...
+
ssh_genkeytypes: <key type>
disable_root: <true/false>
disable_root_opts: <disable root options string>
@@ -169,6 +176,8 @@ for k in GENERATE_KEY_NAMES:
CONFIG_KEY_TO_FILE.update({"%s_private" % k: (KEY_FILE_TPL % k, 0o600)})
CONFIG_KEY_TO_FILE.update(
{"%s_public" % k: (KEY_FILE_TPL % k + ".pub", 0o600)})
+ CONFIG_KEY_TO_FILE.update(
+ {"%s_certificate" % k: (KEY_FILE_TPL % k + "-cert.pub", 0o600)})
PRIV_TO_PUB["%s_private" % k] = "%s_public" % k
KEY_GEN_TPL = 'o=$(ssh-keygen -yf "%s") && echo "$o" root@localhost > "%s"'
@@ -186,12 +195,18 @@ def handle(_name, cfg, cloud, log, _args):
util.logexc(log, "Failed deleting key file %s", f)
if "ssh_keys" in cfg:
- # if there are keys in cloud-config, use them
+ # if there are keys and/or certificates in cloud-config, use them
for (key, val) in cfg["ssh_keys"].items():
- if key in CONFIG_KEY_TO_FILE:
- tgt_fn = CONFIG_KEY_TO_FILE[key][0]
- tgt_perms = CONFIG_KEY_TO_FILE[key][1]
- util.write_file(tgt_fn, val, tgt_perms)
+ # skip entry if unrecognized
+ if key not in CONFIG_KEY_TO_FILE:
+ continue
+ tgt_fn = CONFIG_KEY_TO_FILE[key][0]
+ tgt_perms = CONFIG_KEY_TO_FILE[key][1]
+ util.write_file(tgt_fn, val, tgt_perms)
+ # set server to present the most recently identified certificate
+ if '_certificate' in key:
+ cert_config = {'HostCertificate': tgt_fn}
+ ssh_util.update_ssh_config(cert_config)
for (priv, pub) in PRIV_TO_PUB.items():
if pub in cfg['ssh_keys'] or priv not in cfg['ssh_keys']:
diff --git a/cloudinit/config/tests/test_ssh.py b/cloudinit/config/tests/test_ssh.py
index 0c554414..87ccdb60 100644
--- a/cloudinit/config/tests/test_ssh.py
+++ b/cloudinit/config/tests/test_ssh.py
@@ -10,6 +10,8 @@ import logging
LOG = logging.getLogger(__name__)
MODPATH = "cloudinit.config.cc_ssh."
+KEY_NAMES_NO_DSA = [name for name in cc_ssh.GENERATE_KEY_NAMES
+ if name not in 'dsa']
@mock.patch(MODPATH + "ssh_util.setup_user_keys")
@@ -25,7 +27,7 @@ class TestHandleSsh(CiTestCase):
}
self.test_hostkey_files = []
hostkey_tmpdir = self.tmp_dir()
- for key_type in ['dsa', 'ecdsa', 'ed25519', 'rsa']:
+ for key_type in cc_ssh.GENERATE_KEY_NAMES:
key_data = self.test_hostkeys[key_type]
filename = 'ssh_host_%s_key.pub' % key_type
filepath = os.path.join(hostkey_tmpdir, filename)
@@ -223,7 +225,7 @@ class TestHandleSsh(CiTestCase):
cfg = {}
expected_call = [self.test_hostkeys[key_type] for key_type
- in ['ecdsa', 'ed25519', 'rsa']]
+ in KEY_NAMES_NO_DSA]
cc_ssh.handle("name", cfg, cloud, LOG, None)
self.assertEqual([mock.call(expected_call)],
cloud.datasource.publish_host_keys.call_args_list)
@@ -252,7 +254,7 @@ class TestHandleSsh(CiTestCase):
cfg = {'ssh_publish_hostkeys': {'enabled': True}}
expected_call = [self.test_hostkeys[key_type] for key_type
- in ['ecdsa', 'ed25519', 'rsa']]
+ in KEY_NAMES_NO_DSA]
cc_ssh.handle("name", cfg, cloud, LOG, None)
self.assertEqual([mock.call(expected_call)],
cloud.datasource.publish_host_keys.call_args_list)
@@ -339,7 +341,65 @@ class TestHandleSsh(CiTestCase):
cfg = {'ssh_publish_hostkeys': {'enabled': True,
'blacklist': []}}
expected_call = [self.test_hostkeys[key_type] for key_type
- in ['dsa', 'ecdsa', 'ed25519', 'rsa']]
+ in cc_ssh.GENERATE_KEY_NAMES]
cc_ssh.handle("name", cfg, cloud, LOG, None)
self.assertEqual([mock.call(expected_call)],
cloud.datasource.publish_host_keys.call_args_list)
+
+ @mock.patch(MODPATH + "ug_util.normalize_users_groups")
+ @mock.patch(MODPATH + "util.write_file")
+ def test_handle_ssh_keys_in_cfg(self, m_write_file, m_nug, m_setup_keys):
+ """Test handle with ssh keys and certificate."""
+ # Populate a config dictionary to pass to handle() as well
+ # as the expected file-writing calls.
+ cfg = {"ssh_keys": {}}
+
+ expected_calls = []
+ for key_type in cc_ssh.GENERATE_KEY_NAMES:
+ private_name = "{}_private".format(key_type)
+ public_name = "{}_public".format(key_type)
+ cert_name = "{}_certificate".format(key_type)
+
+ # Actual key contents don"t have to be realistic
+ private_value = "{}_PRIVATE_KEY".format(key_type)
+ public_value = "{}_PUBLIC_KEY".format(key_type)
+ cert_value = "{}_CERT_KEY".format(key_type)
+
+ cfg["ssh_keys"][private_name] = private_value
+ cfg["ssh_keys"][public_name] = public_value
+ cfg["ssh_keys"][cert_name] = cert_value
+
+ expected_calls.extend([
+ mock.call(
+ '/etc/ssh/ssh_host_{}_key'.format(key_type),
+ private_value,
+ 384
+ ),
+ mock.call(
+ '/etc/ssh/ssh_host_{}_key.pub'.format(key_type),
+ public_value,
+ 384
+ ),
+ mock.call(
+ '/etc/ssh/ssh_host_{}_key-cert.pub'.format(key_type),
+ cert_value,
+ 384
+ ),
+ mock.call(
+ '/etc/ssh/sshd_config',
+ ('HostCertificate /etc/ssh/ssh_host_{}_key-cert.pub'
+ '\n'.format(key_type)),
+ preserve_mode=True
+ )
+ ])
+
+ # Run the handler.
+ m_nug.return_value = ([], {})
+ with mock.patch(MODPATH + 'ssh_util.parse_ssh_config',
+ return_value=[]):
+ cc_ssh.handle("name", cfg, self.tmp_cloud(distro='ubuntu'),
+ LOG, None)
+
+ # Check that all expected output has been done.
+ for call_ in expected_calls:
+ self.assertIn(call_, m_write_file.call_args_list)
diff --git a/tests/integration_tests/modules/test_ssh_keys_provided.py b/tests/integration_tests/modules/test_ssh_keys_provided.py
index dc6d2fc1..27d193c1 100644
--- a/tests/integration_tests/modules/test_ssh_keys_provided.py
+++ b/tests/integration_tests/modules/test_ssh_keys_provided.py
@@ -45,6 +45,7 @@ ssh_keys:
A3tFPEOxauXpzCt8f8eXsz0WQXAgIKW2h8zu5QHjomioU3i27mtE
-----END RSA PRIVATE KEY-----
rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/Ho+o3eJISydO2JvIgTLnZOtrxPl+fSvJfKDjoOLY0HB2eOjy2s2/2N6d9X9SGZ4+XnyVeNPjfBXw4IyXoqxhfIF16Azfk022iejgjiYssoUxH31M60OfqJhxo16dWEXdkKP1nac06VOt1zS5yEeooyvEuMJEJSsv3VR/7GKhMX3TVhEz5moLmVP3bIAvvoXio8X4urVC1R819QjDC86nlxwNks/GKPRi/IHO5tjJ72Eke7KNsm/vxHgkdX4vZaHNKhfdb/pavFXN5eoUaofz3hxw5oL/u2epI/pXyUhDp8Tb5wO6slykzcIlGCSd0YeO1TnljvViRx0uSxIy97N root@xenial-lxd
+ rsa_certificate: ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgMpgBP4Phn3L8I7Vqh7lmHKcOfIokEvSEbHDw83Y3JloAAAADAQABAAABAQC0/Ho+o3eJISydO2JvIgTLnZOtrxPl+fSvJfKDjoOLY0HB2eOjy2s2/2N6d9X9SGZ4+XnyVeNPjfBXw4IyXoqxhfIF16Azfk022iejgjiYssoUxH31M60OfqJhxo16dWEXdkKP1nac06VOt1zS5yEeooyvEuMJEJSsv3VR/7GKhMX3TVhEz5moLmVP3bIAvvoXio8X4urVC1R819QjDC86nlxwNks/GKPRi/IHO5tjJ72Eke7KNsm/vxHgkdX4vZaHNKhfdb/pavFXN5eoUaofz3hxw5oL/u2epI/pXyUhDp8Tb5wO6slykzcIlGCSd0YeO1TnljvViRx0uSxIy97NAAAAAAAAAAAAAAACAAAACnhlbmlhbC1seGQAAAAAAAAAAF+vVEIAAAAAYY83bgAAAAAAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgz4SlDwbq53ZrRsnS6ISdwxgFDRpnEX44K8jFmLpI9NAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQMWpiRWKNMFvRX0g6OQOELMqDhtNBpkIN92IyO25qiY2oDSd1NyVme6XnGDFt8CS7z5NufV04doP4aacLOBbQww= root@xenial-lxd
dsa_private: |
-----BEGIN DSA PRIVATE KEY-----
MIIBuwIBAAKBgQD5Fstc23IVSDe6k4DNP8smPKuEWUvHDTOGaXrhOVAfzZ6+jklP
@@ -108,6 +109,18 @@ class TestSshKeysProvided:
"4DOkqNiUGl80Zp1RgZNohHUXlJMtAbrIlAVEk+mTmg7vjfyp2un"
"RQvLZpMRdywBm") in out
+ def test_ssh_rsa_certificate_provided(self, class_client):
+ """Test rsa certificate was imported."""
+ out = class_client.read_from_file("/etc/ssh/ssh_host_rsa_key-cert.pub")
+ assert (
+ "AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgMpg"
+ "BP4Phn3L8I7Vqh7lmHKcOfIokEvSEbHDw83Y3JloAAAAD") in out
+
+ def test_ssh_certificate_updated_sshd_config(self, class_client):
+ """Test ssh certificate was added to /etc/ssh/sshd_config."""
+ out = class_client.read_from_file("/etc/ssh/sshd_config").strip()
+ assert "HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub" in out
+
def test_ssh_ecdsa_keys_provided(self, class_client):
"""Test ecdsa public key was imported."""
out = class_client.read_from_file("/etc/ssh/ssh_host_ecdsa_key.pub")
diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers
index b2464768..9b594a44 100644
--- a/tools/.github-cla-signers
+++ b/tools/.github-cla-signers
@@ -15,6 +15,7 @@ jqueuniet
jsf9k
landon912
lucasmoura
+lungj
manuelisimo
marlluslustosa
matthewruffell