ssh: manage hostkeys and global known_hosts
This commit is contained in:
parent
24bf39dda5
commit
8a9434a384
5 changed files with 124 additions and 9 deletions
5
bundles/ssh/files/ssh_config
Normal file
5
bundles/ssh/files/ssh_config
Normal file
|
@ -0,0 +1,5 @@
|
|||
Host *
|
||||
#UserKnownHostsFile ~/.ssh/known_hosts ~/.ssh/known_hosts.d/%k
|
||||
SendEnv LANG LC_*
|
||||
HashKnownHosts yes
|
||||
GSSAPIAuthentication yes
|
|
@ -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)}
|
||||
|
|
|
@ -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'] = {
|
||||
|
|
|
@ -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}',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
44
libs/ssh.py
44
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(<node with hostname 10.0.0.5>), <salt from ssh-keygen>)'`
|
||||
@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))
|
||||
|
|
Loading…
Reference in a new issue