From 8a9434a384fa00e4af03d0d8a34765649c7607c6 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 27 Mar 2022 16:38:38 +0200 Subject: [PATCH] ssh: manage hostkeys and global known_hosts --- bundles/ssh/files/ssh_config | 5 +++ bundles/ssh/files/sshd_config | 2 +- bundles/ssh/items.py | 61 +++++++++++++++++++++++++++++++---- bundles/ssh/metadata.py | 21 ++++++++++++ libs/ssh.py | 44 ++++++++++++++++++++++++- 5 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 bundles/ssh/files/ssh_config diff --git a/bundles/ssh/files/ssh_config b/bundles/ssh/files/ssh_config new file mode 100644 index 0000000..4db9ee4 --- /dev/null +++ b/bundles/ssh/files/ssh_config @@ -0,0 +1,5 @@ +Host * + #UserKnownHostsFile ~/.ssh/known_hosts ~/.ssh/known_hosts.d/%k + SendEnv LANG LC_* + HashKnownHosts yes + GSSAPIAuthentication yes diff --git a/bundles/ssh/files/sshd_config b/bundles/ssh/files/sshd_config index a84653b..f24ee62 100644 --- a/bundles/ssh/files/sshd_config +++ b/bundles/ssh/files/sshd_config @@ -10,7 +10,7 @@ MaxSessions 255 PubkeyAuthentication yes PasswordAuthentication no ChallengeResponseAuthentication no -AuthorizedKeysFile .ssh/authorized_keys +AuthorizedKeysFile .ssh/authorized_keys UsePAM yes AllowUsers ${' '.join(users)} diff --git a/bundles/ssh/items.py b/bundles/ssh/items.py index e47f47d..3fc5013 100644 --- a/bundles/ssh/items.py +++ b/bundles/ssh/items.py @@ -1,14 +1,61 @@ if not node.metadata.get('FIXME_dont_touch_sshd', False): # on debian bullseye raspberry images, starting the systemd ssh # daemon seems to collide with an existing sysv daemon - files['/etc/ssh/sshd_config'] = { - 'content_type': 'mako', - 'context': { - 'users': sorted(node.metadata.get('ssh/allow_users')), + directories = { + '/etc/ssh': { + 'purge': True, + 'mode': '0755', + } + } + + files = { + '/etc/ssh/moduli': { + 'content_type': 'any', + }, + '/etc/ssh/ssh_config': { + 'triggers': [ + 'svc_systemd:ssh:restart' + ], + }, + '/etc/ssh/ssh_config': { + 'content_type': 'mako', + 'context': { + }, + 'triggers': [ + 'svc_systemd:ssh:restart' + ], + }, + '/etc/ssh/sshd_config': { + 'content_type': 'mako', + 'context': { + 'users': sorted(node.metadata.get('ssh/allow_users')), + }, + 'triggers': [ + 'svc_systemd:ssh:restart' + ], + }, + '/etc/ssh/ssh_host_ed25519_key': { + 'content': node.metadata.get('ssh/host_key/private') + '\n', + 'mode': '0600', + 'triggers': [ + 'svc_systemd:ssh:restart' + ], + }, + '/etc/ssh/ssh_host_ed25519_key.pub': { + 'content': node.metadata.get('ssh/host_key/public') + '\n', + 'mode': '0644', + 'triggers': [ + 'svc_systemd:ssh:restart' + ], + }, + '/etc/ssh/ssh_known_hosts': { + 'content': '\n'.join( + repo.libs.ssh.known_hosts_entry_for(other_node) + for other_node in sorted(repo.nodes) + if other_node != node + and other_node.has_bundle('ssh') + ) + '\n', }, - 'triggers': [ - 'svc_systemd:ssh:restart' - ], } svc_systemd['ssh'] = { diff --git a/bundles/ssh/metadata.py b/bundles/ssh/metadata.py index 5a112a1..3c9a573 100644 --- a/bundles/ssh/metadata.py +++ b/bundles/ssh/metadata.py @@ -1,3 +1,6 @@ +from base64 import b64decode + + @metadata_reactor.provides( 'ssh/allow_users', ) @@ -11,3 +14,21 @@ def users(metadata): ), }, } + + +@metadata_reactor.provides( + 'ssh/host_key', +) +def host_key(metadata): + private, public = repo.libs.ssh.generate_ed25519_key_pair( + b64decode(str(repo.vault.random_bytes_as_base64_for(f"HostKey {metadata.get('id')}", length=32))) + ) + + return { + 'ssh': { + 'host_key': { + 'private': private + '\n', + 'public': public + f' root@{node.name}', + } + }, + } diff --git a/libs/ssh.py b/libs/ssh.py index c4c62d4..01258cc 100644 --- a/libs/ssh.py +++ b/libs/ssh.py @@ -1,6 +1,8 @@ from base64 import b64decode, b64encode -from hashlib import sha3_224 +from hashlib import sha3_224, sha1 from functools import cache +import hmac +from ipaddress import ip_interface from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption @@ -46,3 +48,43 @@ def generate_ed25519_key_pair(secret): # RETURN return (deterministic_privatekey, public_key) + + +#https://www.fragmentationneeded.net/2017/10/ssh-hashknownhosts-file-format.html +# test this: +# - `ssh-keyscan -H 10.0.0.5` +# - take the salt from the ssh-ed25519 entry (first field after '|1|') +# - `bw debug -c 'repo.libs.ssh.known_hosts_entry_for(repo.get_node(), )'` +@cache +def known_hosts_entry_for(node, test_salt=None): + ips = set() + + for network in node.metadata.get('network').values(): + if network.get('ipv4', None): + ips.add(str(ip_interface(network['ipv4']).ip)) + if network.get('ipv6', None): + ips.add(str(ip_interface(network['ipv6']).ip)) + + domains = { + domain + for domain, records in node.metadata.get('dns').items() + for type, values in records.items() + if type in {'A', 'AAAA'} + and set(values) & ips + } + + lines = set() + + for hostname in {node.hostname, *ips, *domains}: + + if test_salt: + salt = b64decode(test_salt) + else: + salt = sha1(node.metadata.get('id').encode()).digest() + + hash = hmac.new(salt, hostname.encode(), sha1).digest() + pubkey = node.metadata.get('ssh/host_key/public') + + lines.add(f'|1|{b64encode(salt).decode()}|{b64encode(hash).decode()} {" ".join(pubkey.split()[:2])}') + + return '\n'.join(sorted(lines))