summaryrefslogtreecommitdiff
path: root/cloudinit/config
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 /cloudinit/config
parent5d4a9a4a50a496d27510f63217bcc0c25d9a8939 (diff)
downloadvyos-cloud-init-66b4be8b6da188a0667bd8c86a25155b6f4f3f6c.tar.gz
vyos-cloud-init-66b4be8b6da188a0667bd8c86a25155b6f4f3f6c.zip
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>
Diffstat (limited to 'cloudinit/config')
-rwxr-xr-xcloudinit/config/cc_ssh.py31
-rw-r--r--cloudinit/config/tests/test_ssh.py68
2 files changed, 87 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)