Merge pull request 'ssh-ca' (#2379) from ssh-ca into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/2379
Reviewed-by: kenji <aks.kenji@protonmail.com>
This commit is contained in:
kenji
2024-11-19 09:46:14 +00:00
17 changed files with 208 additions and 65 deletions

View File

@@ -24,7 +24,6 @@
imports = [
self.clanModules.borgbackup
self.clanModules.localbackup
self.clanModules.sshd
];
clan.core.networking.targetHost = "machine";
networking.hostName = "machine";
@@ -36,6 +35,16 @@
machine.publicKey = builtins.readFile ../lib/ssh/pubkey;
};
services.openssh = {
enable = true;
hostKeys = [
{
path = "/root/.ssh/id_ed25519";
type = "ed25519";
}
];
};
users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ];
systemd.tmpfiles.settings."vmsecrets" = {
@@ -69,6 +78,8 @@
};
};
clan.core.facts.secretStore = "vm";
# TODO: set this backend as well, once we have implemented it.
#clan.core.vars.settings.secretStore = "vm";
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
environment.etc.install-closure.source = "${closureInfo}/store-paths";
@@ -122,7 +133,7 @@
};
};
perSystem =
{ nodes, pkgs, ... }:
{ pkgs, ... }:
{
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
test-backups = (import ../lib/test-base.nix) {

View File

@@ -13,6 +13,7 @@ in
];
documentation.enable = lib.mkDefault false;
nix.settings.min-free = 0;
system.stateVersion = lib.version;
};
# to accept external dependencies such as disko

View File

@@ -1,3 +1,13 @@
---
description = "Enables secure remote access to the machine over ssh"
description = "Enables secure remote access to the machine over ssh."
categories = ["System"]
features = [ "inventory" ]
---
This module will setup the opensshd service.
It will generate a host key for each machine
## Roles
###

View File

@@ -1,25 +1,6 @@
{ config, pkgs, ... }:
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
services.openssh.enable = true;
services.openssh.settings.PasswordAuthentication = false;
services.openssh.hostKeys = [
{
path = config.clan.core.facts.services.openssh.secret."ssh.id_ed25519".path;
type = "ed25519";
}
];
clan.core.facts.services.openssh = {
secret."ssh.id_ed25519" = { };
public."ssh.id_ed25519.pub" = { };
generator.path = [
pkgs.coreutils
pkgs.openssh
];
generator.script = ''
ssh-keygen -t ed25519 -N "" -f $secrets/ssh.id_ed25519
mv $secrets/ssh.id_ed25519.pub $facts/ssh.id_ed25519.pub
'';
};
imports = [ ./roles/server.nix ];
}

View File

@@ -0,0 +1,6 @@
{ ... }:
{
imports = [
../shared.nix
];
}

View File

@@ -0,0 +1,68 @@
{
config,
pkgs,
lib,
...
}:
let
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
signArgs = builtins.concatStringsSep " " (
builtins.map (domain: "-n ${lib.escapeShellArg "${config.clan.core.machineName}.${domain}"}") (
stringSet config.clan.sshd.certificate.searchDomains
)
);
cfg = config.clan.sshd;
in
{
imports = [ ../shared.nix ];
config = {
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.HostCertificate = lib.mkIf (
cfg.certificate.searchDomains != [ ]
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
hostKeys = [
{
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
type = "ed25519";
}
];
};
clan.core.vars.generators.openssh = {
files."ssh.id_ed25519" = { };
files."ssh.id_ed25519.pub".secret = false;
migrateFact = "openssh";
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -f $out/ssh.id_ed25519
'';
};
clan.core.vars.generators.openssh-cert = lib.mkIf (cfg.certificate.searchDomains != [ ]) {
files."ssh.id_ed25519-cert.pub".secret = false;
dependencies = [
"openssh"
"openssh-ca"
];
runtimeInputs = [
pkgs.openssh
pkgs.jq
];
script = ''
ssh-keygen \
-s $in/openssh-ca/id_ed25519 \
-I ${config.clan.core.machineName} \
${builtins.toString signArgs} \
$in/openssh/ssh.id_ed25519.pub
mv $in/openssh/ssh.id_ed25519-cert.pub $out/ssh.id_ed25519-cert.pub
'';
};
};
}

View File

@@ -0,0 +1,49 @@
{
config,
lib,
pkgs,
...
}:
{
options = {
clan.sshd.certificate = {
# TODO: allow per-server domains that we than collect in the inventory
#domains = lib.mkOption {
# type = lib.types.listOf lib.types.str;
# default = [ ];
# example = [ "git.mydomain.com" ];
# description = "List of domains to include in the certificate. This option will not prepend the machine name in front of each domain.";
#};
searchDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "mydomain.com" ];
description = "List of domains to include in the certificate. This option will prepend the machine name in front of each domain before adding it to the certificate.";
};
};
};
config = {
clan.core.vars.generators.openssh-ca =
lib.mkIf (config.clan.sshd.certificate.searchDomains != [ ])
{
share = true;
files.id_ed25519.deploy = false;
files."id_ed25519.pub" = {
deploy = false;
secret = false;
};
runtimeInputs = [
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -f $out/id_ed25519
'';
};
programs.ssh.knownHosts.ssh-ca = lib.mkIf (config.clan.sshd.certificate.searchDomains != [ ]) {
certAuthority = true;
extraHostNames = builtins.map (domain: "*.${domain}") config.clan.sshd.certificate.searchDomains;
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
};
};
}

View File

@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILns3iEVA7MaN+K8qVRFywVOjBZsGyfRuBl26nGL/tXe nixbld@turingmachine

View File

@@ -297,6 +297,10 @@ def _check_can_migrate(
if machine.secret_vars_store.exists(
generator_name, fname, vars_generator["share"]
):
if vars_generator["deploy"]:
machine.secret_vars_store.ensure_machine_has_access(
generator_name, fname, vars_generator["share"]
)
return False
else:
if machine.public_vars_store.exists(
@@ -400,7 +404,7 @@ def generate_vars(
)
machine.flush_caches()
except Exception as exc:
log.exception(f"Failed to generate facts for {machine.name}")
log.error(f"Failed to generate facts for {machine.name}: {exc}") # noqa
errors += [exc]
if len(errors) > 0:
msg = f"Failed to generate facts for {len(errors)} hosts. Check the logs above"

View File

@@ -1,5 +1,4 @@
import subprocess
import tempfile
class Error(Exception):
@@ -20,25 +19,3 @@ def is_valid_age_key(secret_key: str) -> bool:
return True
msg = f"Invalid age key: {secret_key}"
raise Error(msg)
def is_valid_ssh_key(secret_key: str, ssh_pub: str) -> bool:
# create tempfile and write secret_key to it
with tempfile.NamedTemporaryFile() as temp:
temp.write(secret_key.encode("utf-8"))
temp.flush()
# Run the ssh-keygen command with the -y flag to check the key format
result = subprocess.run(
["ssh-keygen", "-y", "-f", temp.name],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
if result.stdout != ssh_pub:
msg = f"Expected '{ssh_pub}' got '{result.stdout}' for ssh key: {secret_key}"
raise Error(msg)
return True
msg = f"Invalid ssh key: {secret_key}"
raise Error(msg)

View File

@@ -9,7 +9,7 @@ from clan_cli.machines.machines import Machine
from clan_cli.secrets.folders import sops_secrets_folder
from fixtures_flakes import FlakeForTest
from helpers import cli
from helpers.validator import is_valid_age_key, is_valid_ssh_key
from helpers.validator import is_valid_age_key
if TYPE_CHECKING:
from age_keys import KeyPair
@@ -85,7 +85,6 @@ def test_generate_secret(
assert store2.exists("", "password-hash")
assert store2.exists("", "user-password")
assert store2.exists("", "user-password-hash")
assert store2.exists("", "ssh.id_ed25519")
assert store2.exists("", "age.key")
assert store2.exists("", "zerotier-identity-secret")
@@ -97,11 +96,6 @@ def test_generate_secret(
assert age_secret.isprintable()
assert is_valid_age_key(age_secret)
# Assert that the ssh key is valid
ssh_secret = store2.get("", "ssh.id_ed25519").decode()
ssh_pub = machine_get_fact(test_flake_with_core.path, "vm2", "ssh.id_ed25519.pub")
assert is_valid_ssh_key(ssh_secret, ssh_pub)
# Assert that root-password is valid
pwd_secret = store2.get("", "password").decode()
assert pwd_secret.isprintable()

View File

@@ -10,7 +10,6 @@ from clan_cli.nix import nix_shell
from clan_cli.ssh import HostGroup
from fixtures_flakes import ClanFlake
from helpers import cli
from helpers.validator import is_valid_ssh_key
@pytest.mark.impure
@@ -90,14 +89,8 @@ def test_upload_secret(
assert store.exists("", "password-hash")
assert store.exists("", "user-password")
assert store.exists("", "user-password-hash")
assert store.exists("", "ssh.id_ed25519")
assert store.exists("", "zerotier-identity-secret")
# Assert that the ssh key is valid
ssh_secret = store.get("", "ssh.id_ed25519").decode()
ssh_pub = machine_get_fact(flake.path, "vm1", "ssh.id_ed25519.pub")
assert is_valid_ssh_key(ssh_secret, ssh_pub)
# Assert that root-password is valid
pwd_secret = store.get("", "password").decode()
assert pwd_secret.isprintable()

View File

@@ -0,0 +1,4 @@
{
"publickey": "age1ez6xlcxl5k2uekcjvsu5wjca29f0j3lml0kq8fnvnkugvnj4pyjsyzuc93",
"type": "age"
}

View File

@@ -0,0 +1,20 @@
{
"data": "ENC[AES256_GCM,data:4Rx8J1mQjaJTmpN5ZBWWKBdObgjd4qjuEWkmXHHWeBUPi0nvYL00vFmdu2LeJYEmZaEP4urEGe9TlACB8JoC6ahI9iuXCWnzzA4=,iv:wOWFcNDU8+ur8sZRxlMr+TlzZmbsvE5o/FfXnINBmag=,tag:jHf+rpnSca9T0ntN93wSaw==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
"azure_kv": null,
"hc_vault": null,
"age": [
{
"recipient": "age17n64ahe3wesh8l8lj0zylf4nljdmqn28hvqns2g7hgm9mdkhlsvsjuvkxz",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpNE9nbmpoanhERGxuVUFL\ndnFPaVI1eXNqNENjeHVNV0Z0Rmx6NDJXNWtFCjdlOXhiU3hTRWRWVjJGMXpUTGtE\naUd5VUNUc2RML29ZSkJYWEd0VnJOc1UKLS0tIG9FdlNsMk5ETmRzaGEzRmN4allC\ncUMzd2N3dW03N0VvWit1eE9OVVRFcWsKzocpuGOlf3kYxbUDvVHP7G27G5n8vWFg\n5Jjf4qaW+ioXpqD0moHVVygbXXB6zkfrJraMaC9Sccdl8eLJNWuE0g==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2024-11-14T16:33:56Z",
"mac": "ENC[AES256_GCM,data:7G6svIuQrI2O5JZ3thNH6om8n3sdoTukfRQqOZ5/x28/BuVyfybsPP2So5MPlT4/OGPvhBGVWhHZ9W201zXOrzAI5T+bR2uX6VDeISRMscniVehnuAwipwCSqkBYO+FfvGjMSh8/kyF20PJR/Ta28qTcO7FKph2BgcOjhl1A0dM=,iv:BpqQT+rRk5mdCLY0dMoKyevhkJZ/dczeQPVSn+qzA9I=,tag:/gxOqChMYz4M65Z1SIcMsg==,type:str]",
"pgp": null,
"unencrypted_suffix": "_unencrypted",
"version": "3.9.1"
}
}

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILnWhu8zt/mD+TlIKr8Req4c+BCgqYuDOcZfmzj6kflF nixbld@turingmachine

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/test-backup

View File

@@ -0,0 +1,24 @@
{
"data": "ENC[AES256_GCM,data:KYuyTnrrzYUFWh+mXiy2puoW4BftckADAroO4FzpH6OJ9Hd9wUEj0iGcOqkb/VDmwaJxw5L7rqBHNNiKFXRNPsybTUUUQHFx35TWpS+sX16Y9F3vXF/LS+7B7R+EioFD2WViPOFvUM/8SJwF87gtCs8r8P46YQhpfS01GgGlFiPyOB0S6l/2RzGn4oJyseZIR+rF7SGDigIvFYyHzkufIKnTyaA3dkS+wLwZZLDmkzv9iNeHMqSecUOPlvqPgfizvjV8exNjVlypXAhX2fePHZhC+DOLdUrFy0aLfY+tejfJNKwwcc3krNwT94c0cSNvgylYVPyoU308eAzaDZufjSmZhc+kYPSO8bwemoX6eBnDy7vd3UzAQ/VfmPKoBuL8KzjSk02wUC5/Q8su1Xk397O12SS3GhGm3BAzMOf/QgqZK7eQsyOX2iBefoZTQ4rIED/szp2++KU1BFppXpvK1sdvcLg3g9v5pKvTgMGh25rJe/LI+jYFlPMdGgv1tY1GI504SM8B8l8a/9Ti1b7apO6QrUeT3Q/0jpJu,iv:YWZDzxpnrkTOmDJ/+0iO55dtbvX+UjqGctDduF3c1Sk=,tag:Q7e/9+0ClQn5bCGxoW+lOQ==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
"azure_kv": null,
"hc_vault": null,
"age": [
{
"recipient": "age17n64ahe3wesh8l8lj0zylf4nljdmqn28hvqns2g7hgm9mdkhlsvsjuvkxz",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQY1AzdmJGWitIYWlEUVM2\nTkRNeVZRYXg5SXRqdlRSOG9NaE8wNTZrU1FBCkRPOGE0bEZsMHdCSUdXQVE1UmVT\nMk9ZSHNZY0FXRG5WQVFaLzY3Ry9GeVEKLS0tIENHM09SWUJqMFJZbjBzdmp4eXlo\nNGJ2QWZaR1NiKzNVQXRVUnJSc2JycG8K9czRLJeCJp8vmPUY339x8Lvux3WDbrdJ\nbp7ynECENrgyP+5CdxopUQMGWptdjTYVho3nKv5NL+rjUZkfagV8HA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1ez6xlcxl5k2uekcjvsu5wjca29f0j3lml0kq8fnvnkugvnj4pyjsyzuc93",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4QVRianYxZ01QWEFSckdl\nd0taZlBWbTlkZHVmd2RXdmxYUjV3K3hHNzBJClMyQktNNWYrQTZkY09GL2dmYkRp\nOGl4WG85OHgxUWdDZVV3R1lDdjRZK2MKLS0tIHN3Ykk0OXA2c0o0a0RkTTZoZHZU\nTWJac09PM1J6clk2bDZxQVFyc29KbG8KZjHj109+FTmldFcdbLEGwSULWt3fLDXf\nYkeWVRa0rB2OtDxLrE6eq4QC1uGZ7KyCwAVqcMyriRMkje3121FBTQ==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2024-11-14T16:33:58Z",
"mac": "ENC[AES256_GCM,data:vWP3TwsjWwWg+gByhdcGVRR08jnH+AS8cOiuNic8AN2ozGZMP0C6mc5EER44wZiGeBWPdoTjqv9tQld4Gc5sWKuZ6QziNIqV/4WBwLwOTRmpVkJNpivJpCTdVyzQKv43xVk/e/ED8wTG6X9M83IGhMX6tUw9XxbCuJiZw61N/6Q=,iv:PTiwi9l16uKGQtNWXovA0Gjzg45O+T75BPT35gOzZLM=,tag:3AXwu/Va7yMsYlpd9Ss6hw==,type:str]",
"pgp": null,
"unencrypted_suffix": "_unencrypted",
"version": "3.9.1"
}
}