WIP janus secrets
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
---
|
||||
description: "Use when modifying the Janus host, Janus step-ca deployment, Step SSH CA, mTLS issuance, CA bootstrap artifacts, or Janus SOPS secrets. Covers idiomatic Nix, secret placement, and troubleshooting."
|
||||
name: "Janus Host Instructions"
|
||||
applyTo: "modules/hosts/janus/**, modules/services/step-ca/step-ca.nix, modules/features/step-client.nix"
|
||||
---
|
||||
|
||||
# Janus Host Instructions
|
||||
|
||||
Use this instruction when changing Janus host wiring, Janus CA material, or the shared Step CA module that Janus consumes.
|
||||
|
||||
## Host Intent And Boundaries
|
||||
|
||||
- Treat Janus as the homelab certificate authority host. It runs `step-ca` for X.509 issuance, SSH user and host certificates, and mTLS certificates.
|
||||
- Assume Janus CA endpoints are private/LAN-only unless the task explicitly changes the exposure model.
|
||||
- Keep Janus as a concrete host under `modules/hosts/janus`; keep reusable behavior in shared modules such as `modules/services/step-ca/step-ca.nix`, `modules/services/step-ca/ssh-host.nix`, `modules/features/step-client.nix`, and `modules/features/mtls.nix`.
|
||||
- Preserve host identity unless the task explicitly renames the deployed CA: `hostname = "janus"`, `flake.nixosConfigurations."${hostname}"`, and `networking.hostName = hostname`.
|
||||
- Keep Janus-specific paths, DNS names, IP SANs, and CA artifacts in the Janus host area or in Janus-specific option values. Do not move them into generic shared modules unless they become a deliberate module interface.
|
||||
|
||||
## Required Module Composition
|
||||
|
||||
When editing `modules/hosts/janus/default.nix`, preserve this composition unless the task explicitly changes Janus architecture:
|
||||
|
||||
- `nixos.lxc` for the container host shape.
|
||||
- `nixos.mysops` with `sops.defaultSopsFile = ./secrets.yaml` for host-local encrypted CA runtime material.
|
||||
- `nixos.step-ca` to run the CA daemon and render `/etc/smallstep/ca.json` from Nix plus SOPS paths.
|
||||
- `nixos.step-client` to install Step trust bootstrap defaults and root CA material.
|
||||
- `nixos.step-ssh-host` so Janus itself participates in SSH host certificate flow.
|
||||
- `nixos.mtls` so Janus can issue and renew its own mTLS certificate bundle.
|
||||
|
||||
## Step CA Deployment Pattern
|
||||
|
||||
- Prefer structured Nix values rendered with `builtins.toJSON` over hand-written JSON strings for `ca.json`.
|
||||
- Prefer the NixOS `services.step-ca.settings` interface for normal `ca.json` settings. Use a `sops.templates`-rendered config only when the rendered JSON needs sops-nix placeholders or runtime secret paths.
|
||||
- Keep secret-bearing `ca.json` content rendered through `sops.templates` when it includes values from `config.sops.placeholder`; do not put decrypted private keys, passwords, or provisioner encrypted private-key values directly in the Nix store.
|
||||
- Continue using `services.step-ca` for the daemon contract: `enable`, `settings`, `address`, `port`, `openFirewall`, `package`, and `intermediatePasswordFile`.
|
||||
- Remember that the NixOS module overrides `services.step-ca.settings.address` with `services.step-ca.address` plus `services.step-ca.port`. Change the high-level address/port options when changing the listener.
|
||||
- Keep the intermediate password as a runtime string path from `config.sops.secrets.<name>.path`; do not use a Nix path literal or the password file can be copied to the globally readable Nix store. Do not pass passwords through command-line arguments or environment variables.
|
||||
- Account for upstream NixOS service behavior when debugging: the module writes `/etc/smallstep/ca.json`, starts `step-ca.service` as `Type=notify`, sets `DynamicUser = true`, uses `StateDirectory = "step-ca"`, and passes `intermediatePasswordFile` through systemd `LoadCredential`.
|
||||
- If `step-ca` cannot read key files referenced by `ca.json`, check sops-nix owner/group/mode and systemd credential handling before loosening permissions. The password file path and CA signing key paths are separate contracts.
|
||||
- Keep Step CA runtime state in `/var/lib/step-ca/db` unless the task explicitly migrates storage. If moving away from embedded Badger, document database backup and HA implications.
|
||||
- Keep `root`, `crt`, `key`, `ssh.hostKey`, and `ssh.userKey` in `ca.json` pointing at public cert paths or sops-nix materialized secret paths as appropriate.
|
||||
- Use an ACME provisioner for automated X.509 issuance, an `SSHPOP` provisioner for SSH certificate renewal/rekey behavior, and the `admin` JWK provisioner for explicit administrative or scripted issuance. Ensure JWK claims include `enableSSHCA = true` when it is expected to sign SSH certificates.
|
||||
- Treat Nix and SOPS as the source of truth. If `step ca provisioner add`, `remove`, or `update` is used for investigation, copy the intended result back into Nix/SOPS and reload or restart `step-ca`; do not leave live `ca.json` drift on the host.
|
||||
- Use `authority.claims` or provisioner-level claims for certificate lifetimes, renewal behavior, and SSH CA enablement. Avoid `allowRenewalAfterExpiry` unless the task explicitly accepts the added risk for intermittently connected clients.
|
||||
- Add `authority.policy` when constraining which X.509 SANs or SSH principals Janus may issue. This is mandatory for any move away from private/LAN-only operation.
|
||||
|
||||
## CA Material And Secret Placement
|
||||
|
||||
Store each kind of material in the narrowest place that matches who needs it:
|
||||
|
||||
- `modules/hosts/janus/secrets.yaml`: Janus runtime CA secrets used by `step-ca`, including `janus/ca_password`, `janus/intermediate_ca_key`, `janus/ssh_host_ca_key`, `janus/ssh_user_ca_key`, and `janus/admin_provisioner_encrypted_key`.
|
||||
- `keys/secrets.yaml`: shared provisioner credential `janus/admin_jwk`, because client and host certificate modules outside Janus consume it to request certs.
|
||||
- `modules/hosts/janus/root_ca.crt`, `intermediate_ca.crt`, `fingerprint`, `ssh_user_ca_key.pub`, and `ssh_host_ca_key.pub`: public trust artifacts that may be committed and reviewed.
|
||||
- Offline secret storage only: `root_ca_key` and the root key password. The running CA should not need the root private key for normal operation.
|
||||
|
||||
When editing secrets:
|
||||
|
||||
- Use SOPS commands (`sops`, `edit-secrets`, or `sops set`) rather than editing `ENC[...]` payloads.
|
||||
- Use YAML literal blocks for PEM or OpenSSH private keys so newlines are preserved.
|
||||
- After changing Janus recipient rules, run `sops --config .sops.yaml updatekeys -y modules/hosts/janus/secrets.yaml` so the host-local file is actually rewrapped.
|
||||
- Keep `.sops.yaml` scoped: Janus host runtime secrets should match `modules/hosts/janus/secrets.yaml`; shared credentials should match `keys/secrets.yaml`.
|
||||
|
||||
## Public Trust Artifacts
|
||||
|
||||
- If `root_ca.crt` changes, update `fingerprint` from `step certificate fingerprint <root_ca.crt>` and update `modules/features/step-client.nix` defaults in the same change.
|
||||
- If `intermediate_ca.crt` changes, confirm it still chains to the committed root and that the running CA uses the matching `intermediate_ca_key` from SOPS.
|
||||
- If SSH CA keys rotate, update the matching public key files and every SSH trust consumer, including `modules/services/ssh.nix` known-host CA values and `TrustedUserCAKeys` behavior.
|
||||
- Treat `README.md` bootstrap commands as part of the operational contract. Keep them aligned with secret names, public artifact paths, CA URL, DNS names, and IP SANs.
|
||||
|
||||
## mTLS And Client Trust
|
||||
|
||||
- Janus mTLS values should stay aligned with Step CA identity: update `step-ca.dnsNames`, `mtls.subject`, `mtls.san`, and Step client defaults together when CA names or addresses change.
|
||||
- `modules/features/step-client.nix` is the shared bootstrap surface. Its default CA URL, root certificate, and fingerprint should describe Janus unless the repo intentionally migrates to another CA.
|
||||
- mTLS certificate files are runtime outputs under the configured cert directory, not tracked source files. Do not store mTLS private keys in Git or the Nix store.
|
||||
|
||||
## Step/Nix Troubleshooting Checklist
|
||||
|
||||
Start with the smallest check that matches the change:
|
||||
|
||||
1. Host evaluation: `nix eval .#nixosConfigurations.janus.config.system.build.toplevel.drvPath`
|
||||
2. Build planning: `nix build .#nixosConfigurations.janus.config.system.build.toplevel --no-link --dry-run`
|
||||
3. Secret contract: confirm every `step-ca.secrets.*` value has a matching `sops.secrets` declaration and a matching key in `modules/hosts/janus/secrets.yaml`.
|
||||
4. Rendered config: on the host, inspect `/etc/smallstep/ca.json` or `/etc/step-ca` paths without copying secret content into logs.
|
||||
5. Service health: check `systemctl status step-ca.service`, `journalctl -u step-ca.service`, and `step ca health --ca-url https://janus.john-stream.com` after trust is bootstrapped.
|
||||
6. Provisioners: use `step ca provisioner list` to confirm ACME, SSHPOP, and admin JWK availability.
|
||||
7. SSH cert flow: use `ssh-host-cert-check`, `systemctl status step-ssh-host-renew.service`, and `systemctl status step-ssh-host-renew.timer`.
|
||||
8. mTLS flow: use `mtls-check`, `mtls-generate`, and `systemctl status mtls-renew.timer` for host certificate renewal issues.
|
||||
|
||||
## Change Safety Rules
|
||||
|
||||
- Do not deploy a root private key to Janus unless the task explicitly changes the trust model.
|
||||
- Do not rename `janus/admin_jwk` without updating all consumers in the same change.
|
||||
- Do not expose ACME or `/provisioners` endpoints publicly without an explicit migration away from the private/LAN-only model plus policy and endpoint exposure review.
|
||||
- Do not use certificate templates casually; if templates are added, keep them narrow and use `toJson` for user-controlled values.
|
||||
- If changing certificate durations, prefer short-lived leaf certs with automated renewal over long-lived leaf certs.
|
||||
- If changing service ports or proxying, account for Step renewal behavior: mTLS renewal is sensitive to layer-7 proxies unless configured for token-based renewal.
|
||||
@@ -3,10 +3,12 @@ keys:
|
||||
- &john-pc age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
|
||||
- &test-nix age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
|
||||
- &soteria age1h0prahyukq4l564yqwgcpg3g6gdrjflk0suklussjjrjstxd9uesws8633
|
||||
- &janus age1n94nkvmzezdj08t4dpwukd7s5k4cx2rldwvx9x5qhx3xhqm48yhq6ejldt
|
||||
creation_rules:
|
||||
- path_regex: modules/hosts/janus/secrets\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *janus
|
||||
- *john-pc
|
||||
- *test-nix
|
||||
- path_regex: keys/secrets\.yaml$
|
||||
@@ -16,6 +18,7 @@ creation_rules:
|
||||
- *john-pc
|
||||
- *test-nix
|
||||
- *soteria
|
||||
- *janus
|
||||
- path_regex: soteria/secrets\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
|
||||
+29
-20
@@ -11,40 +11,49 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhbmduOWR1ZloxWjRjTjFp
|
||||
ZFpvUFA2cStzYllUa1BhSmJBYWVUSmNGblZ3Cmw3TnVBSGtwaDV4ZlRRT2h6OWw0
|
||||
bGd6dUQ2eE1QVWNTTitSSG80NVhraU0KLS0tIHRVMXFFRGh5cjlwNXpIb0F4TnF3
|
||||
Ykplcm1DWm04RExHYnRjQjdCOVhLaE0K/VGQ/58QWOAWPToQt22W4iBX7rrh/lxL
|
||||
Via5/T25c64Eh6FXzar+z9zdHjS4s7PLsf6iJIY84xpKCpSAkcaquw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuQytmUDE3NVg1RWlYcjVq
|
||||
RVp6dHlqOHlNdXAyWkNlOVlGQlIwZERpdFRrCk1CTW84Q1B0MEFKTmpFM2FNS3lE
|
||||
NDY4b0k3dTdST1VyQlFZVUhGSlpudDgKLS0tIHlVZDBrd0VFSWRsUGtpVjVIYnUx
|
||||
VGc0WjlNSzE1U2h2cFNZekxoNUhSdVUKYpl5ZlZpH5zvfNUVrPdd+dqwZ16zJn7D
|
||||
cbc5xZLtDpoeG3ZQQeV9T4WGTKycZN8Wo13Iw7BtQL/zUnUU8L/y8g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1f6drjusg866yscj8029tk4yfpgecklrvezldm02ankm6h8nnwu5s2u6ahy
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkdEVYYzR2M1hDSmR6aVZO
|
||||
WCtBUTBVUU96SitsTFlBT2IzQjk2Q21LV2tvCkRxVEVESVFsWlczdU5CT2V1a0cw
|
||||
MUxXNFJFa1BvRFdGbVpkdDRTYVhROUkKLS0tIHBMaGFTRmgrYnkraGt3YkRCMXhw
|
||||
Rk9GMUhXcWhYL3NXdmRZN0FtQjR2bU0Kql1oAOuJDJ2jcR+WcibBiFiVcMEBbXVS
|
||||
cxydfdl3/la1tqiMd6rnDh/iB5hwjeeULwrgE/5Qjc7w23hMN9AF1g==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6czRVNUhGaFV1U0dUKzNp
|
||||
YVl0TGZBTnBIaS96dWo2S3BZUFBSclhITkJNCkZZU0dCdnA4d1VVOFRjU2JueG1C
|
||||
MndFazdFZG5CbEdPU2xSS24xNnBHQ2sKLS0tIFh3TWxydi96alBKRkpqa3VKekUx
|
||||
ck5hbU1ValNJU2d1cTVTZU10NSsxVkEK5/dMd08yTRKNmunA3u7RYtJsaYVRD/cG
|
||||
cXL2g5ewWl/vhFH71pwCpXIyEhKCF1U6HR9VlesnwU8ZOON59n9FKg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArTnc1ZEorSDlZSW1qY3Zu
|
||||
NExrYXgyQjRIQUNRYlFXZCt4bXcxRndzWW40CjNCM3p0VDcwbENoTGxFNGZRR3lM
|
||||
R0pnSlluTUpiNk91d0hlci9sOUtBYjQKLS0tIGp2eWpReExSNTdCS0RBQ1F5dDNt
|
||||
eWhuak00dTFZV0k1WEREQkRPWUFmc00KQ13ormwGmzwPxvId8WXe1PY4FG3g1Juw
|
||||
KnBf20sSRkJ/oj3iEhfd8ZmXeoK1Xc44aeCcVe7NEzVtMhjIdzcNqg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWVWlDaTdpK2t1Z1pTbmZO
|
||||
V2o5VW9oa3c3eE9Yc2lzaWtJdk5KcWZFTUQ4CkI2UnN3NWxQOUVwaDFXWmY0ODBU
|
||||
NXFHeEYwYnUvVkRmNzFQZVNUZHd3bjgKLS0tIDA2Q2J2bnVGUHJ6UEIzNHpmbTRL
|
||||
U284VStkZ3MyS0x2RTA4akJPRmJtbU0KSKpSQDPiC+m6ta9kYb5J2uYstW2fz2Xq
|
||||
zVYs/F1727hasnwTOnR+rdN2k72dpxof1T6CxRKWZ3xjkvFqis2Nhg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWTHN6TDBHdFU1L1lEQzg4
|
||||
TW1vT08vYnp3bzBzSFlxbGpDSFNycFYycFJRCnYvcjVQOC9rZTFXb0lHQWFUVVhZ
|
||||
ZFVtM09zWW9GZzNMRnBHdHNFNnZHdTgKLS0tIFh1b1pnKzJQUWg4YkRIWVFnRDNz
|
||||
bXRzY1piak43N0dMZFQ1WWdSZjBBT2cKujwWnuf7LFZe1TIu0R+Vc+HGCYqaeRkD
|
||||
jBuPRyN7cqCZ5cK9492BVLg48toU/djCSs1w33aLeTF5Ug/Swduqkg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPbFUxTmhablI5SEczeWlV
|
||||
Q20vYzFGR21GUzNQZVdSb3hCOXNNMUN5K3dNCi9FeEpPcHJCV0t1eU5VMmNzclla
|
||||
eWdWL2RVMXZMbHhPN2Z1NHdBRnBpQnMKLS0tIG5XR1QrOXJQNHRNQzZvU0hVaFpv
|
||||
eU82a3Y4enZpUTJIU0dObkkxa1J5K0kKoiSmt5LcM8U82pwwsvMBsgEbQ6u3+156
|
||||
GFXQFp2+gk8zsxtzCkW1nl52Bw3GfXS7cJTW3jOVBw/YquDTc8nhXQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1h0prahyukq4l564yqwgcpg3g6gdrjflk0suklussjjrjstxd9uesws8633
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBETlZVQ2RyT21KeWtNRFd6
|
||||
ZytTWStEdFZkaG9jZzJkU253SzE1VUgvQkNNCk1QTDF6dGo4aXF2TmhtRlcrM3dk
|
||||
Z25kVXZxQTlJY1pIMXZQOU9FdWN6T1UKLS0tIGhBU0FVNHd6cWhiZzh3Skp6Nzdz
|
||||
SHhEMndWN1Z1TlRoajRnSEZ1UmFvbmsKR79t34F3rjQg6IzQEIc7W/VUjpHLgFus
|
||||
wlMkfHfZqUDo/ORvOJmT337l2XuK/spbsNlYqfupCWUbvtMZNhnkAg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1n94nkvmzezdj08t4dpwukd7s5k4cx2rldwvx9x5qhx3xhqm48yhq6ejldt
|
||||
lastmodified: "2026-03-15T15:06:29Z"
|
||||
mac: ENC[AES256_GCM,data:cF/TJ8VkzrHRUrO5iGdRdlFtqV/5EQ15JwQKIywJvsh0NERK67T21czSP7923MiL0u5QTVPn/rO8R5E/8gBu3r8+fLq+CFl9PDQHEX2JhnYOD5WZR412WMZq3MVR94IMTOrQANMVpS4uhMyvnrqOe4AenxLDyzrYhkwf1KQh4w0=,iv:Qwy8z4uXGMlf+kTMNiE42M9l8LtSJ+O7diknRrsSeYI=,tag:qlCY9r8HnEDmq/jw59C/sg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
|
||||
@@ -144,6 +144,33 @@ in
|
||||
type = lib.types.str;
|
||||
default = "/etc/step-ca/certs";
|
||||
};
|
||||
bootstrap = {
|
||||
enable = lib.mkOption {
|
||||
description = "Enable initial mTLS issuance when cert material is missing or invalid.";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
wantedBy = lib.mkOption {
|
||||
description = "systemd targets that should pull in mtls-bootstrap.service.";
|
||||
type = with lib.types; listOf str;
|
||||
default = [ "multi-user.target" ];
|
||||
};
|
||||
after = lib.mkOption {
|
||||
description = "systemd units/targets that mtls-bootstrap.service should run after.";
|
||||
type = with lib.types; listOf str;
|
||||
default = [ "network-online.target" ];
|
||||
};
|
||||
wants = lib.mkOption {
|
||||
description = "systemd units/targets that mtls-bootstrap.service should pull in.";
|
||||
type = with lib.types; listOf str;
|
||||
default = [ "network-online.target" ];
|
||||
};
|
||||
provisionerPasswordFile = lib.mkOption {
|
||||
description = "Optional path passed to mtls-generate as --provisioner-password-file for noninteractive issuance.";
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
certReaders = lib.mkOption {
|
||||
description = "";
|
||||
type = lib.types.listOf lib.types.str;
|
||||
@@ -179,6 +206,38 @@ in
|
||||
mtlsRenewWrapper.outputs.systemd-system
|
||||
];
|
||||
|
||||
systemd.services.mtls-bootstrap = lib.mkIf cfg.bootstrap.enable {
|
||||
description = "Issue initial mTLS certificate if missing or invalid";
|
||||
wantedBy = cfg.bootstrap.wantedBy;
|
||||
after = cfg.bootstrap.after;
|
||||
wants = cfg.bootstrap.wants;
|
||||
path = with pkgs; [ coreutils step-cli ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.renew.user;
|
||||
Group = cfg.renew.group;
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
if [ -s "${cfg.certFile}" ] \
|
||||
&& [ -s "${cfg.keyFile}" ] \
|
||||
&& [ -s "${cfg.bundleFile}" ] \
|
||||
&& step certificate inspect "${cfg.certFile}" >/dev/null 2>&1; then
|
||||
echo "mTLS certificate already exists"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Issuing initial mTLS certificate"
|
||||
cmd=(/run/current-system/sw/bin/mtls-generate)
|
||||
${lib.optionalString (cfg.bootstrap.provisionerPasswordFile != null) ''
|
||||
cmd+=(--provisioner-password-file "${cfg.bootstrap.provisionerPasswordFile}")
|
||||
''}
|
||||
|
||||
"''${cmd[@]}"
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.mtls-renew = lib.mkIf cfg.renew.enable {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
|
||||
@@ -68,6 +68,7 @@ in
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.certDir} 0750 root root -"
|
||||
"L+ ${cfg.certDir}/root_ca.crt - - - - ${cfg.root}"
|
||||
"d /root/.step 0700 root root -"
|
||||
"d /root/.step/config 0700 root root -"
|
||||
"d /root/.step/certs 0700 root root -"
|
||||
|
||||
@@ -5,6 +5,13 @@ Generate passwords:
|
||||
mkdir -p /tmp/janus-step-ca-bootstrap && chmod 700 /tmp/janus-step-ca-bootstrap && cd /tmp/janus-step-ca-bootstrap && umask 077 && openssl rand -base64 48 > ca_password.txt && openssl rand -base64 48 > admin_jwk_password.txt
|
||||
```
|
||||
|
||||
Generate the Janus OpenSSH host key that Nix will materialize from SOPS at
|
||||
`/etc/ssh/ssh_host_ed25519_key`:
|
||||
|
||||
```shell
|
||||
ssh-keygen -t ed25519 -N '' -C [email protected] -f /tmp/janus-step-ca-bootstrap/ssh_host_ed25519_key
|
||||
```
|
||||
|
||||
Bootstrap CA materials with SSH enabled:
|
||||
```shell
|
||||
STEPPATH=/tmp/janus-step-ca-bootstrap/step step ca init --name Janus --dns janus.john-stream.com --dns 192.168.1.244 --address :443 --provisioner admin --password-file /tmp/janus-step-ca-bootstrap/ca_password.txt --provisioner-password-file /tmp/janus-step-ca-bootstrap/admin_jwk_password.txt --ssh --deployment-type standalone --with-ca-url https://janus.john-stream.com
|
||||
@@ -17,9 +24,24 @@ Insert generated runtime CA material into `modules/hosts/janus/secrets.yaml` und
|
||||
- `/tmp/janus-step-ca-bootstrap/step/secrets/ssh_host_ca_key` -> `ssh_host_ca_key`
|
||||
- `/tmp/janus-step-ca-bootstrap/step/secrets/ssh_user_ca_key` -> `ssh_user_ca_key`
|
||||
- `/tmp/janus-step-ca-bootstrap/step/config/ca.json` -> `admin_provisioner_encrypted_key` (copy `authority.provisioners[].encryptedKey` for the `admin` JWK provisioner)
|
||||
- `/tmp/janus-step-ca-bootstrap/ssh_host_ed25519_key` -> `ssh_host_ed25519_key`
|
||||
|
||||
If you are only validating wiring first, `admin_provisioner_encrypted_key` can be an encrypted placeholder and replaced later.
|
||||
|
||||
The checked-in `ssh_host_ed25519_key` value is also only an encrypted placeholder.
|
||||
Replace it before deploying Janus, otherwise OpenSSH and SSH host certificate
|
||||
issuance will fail because the materialized host key is not a valid private key.
|
||||
|
||||
For multiline secret values, avoid putting private key contents in shell history.
|
||||
One safe non-interactive pattern is to JSON-encode the file content and pass that
|
||||
temporary JSON file to `sops set --value-file`:
|
||||
|
||||
```shell
|
||||
jq -Rs . /tmp/janus-step-ca-bootstrap/ssh_host_ed25519_key > /tmp/janus-step-ca-bootstrap/ssh_host_ed25519_key.json
|
||||
sops --config .sops.yaml set --value-file modules/hosts/janus/secrets.yaml '["janus"]["ssh_host_ed25519_key"]' /tmp/janus-step-ca-bootstrap/ssh_host_ed25519_key.json
|
||||
rm -f /tmp/janus-step-ca-bootstrap/ssh_host_ed25519_key.json
|
||||
```
|
||||
|
||||
If rotating provisioner password, also set:
|
||||
|
||||
- `/tmp/janus-step-ca-bootstrap/admin_jwk_password.txt` -> `janus.admin_jwk` in `keys/secrets.yaml`
|
||||
@@ -27,6 +49,7 @@ If rotating provisioner password, also set:
|
||||
Secret source-of-truth after this split:
|
||||
|
||||
- `modules/hosts/janus/secrets.yaml`: Janus runtime CA secrets (`ca_password`, `intermediate_ca_key`, `ssh_host_ca_key`, `ssh_user_ca_key`, `admin_provisioner_encrypted_key`)
|
||||
- `modules/hosts/janus/secrets.yaml`: Janus OpenSSH host private key (`ssh_host_ed25519_key`) materialized to `/etc/ssh/ssh_host_ed25519_key`
|
||||
- `keys/secrets.yaml`: shared Janus provisioner secret (`janus.admin_jwk`) consumed by `step-ssh-host` across hosts
|
||||
|
||||
Then update public artifacts in repo from generated output:
|
||||
@@ -38,6 +61,26 @@ Then update public artifacts in repo from generated output:
|
||||
step certificate fingerprint /tmp/janus-step-ca-bootstrap/step/certs/root_ca.crt
|
||||
```
|
||||
- `modules/hosts/janus/ssh_user_ca_key.pub` from `/tmp/janus-step-ca-bootstrap/step/certs/ssh_user_ca_key.pub`
|
||||
- `modules/hosts/janus/ssh_host_ca_key.pub` from `/tmp/janus-step-ca-bootstrap/step/certs/ssh_host_ca_key.pub`
|
||||
|
||||
## First boot checks
|
||||
|
||||
After switching Janus, verify the declarative bootstrap units instead of running
|
||||
ad hoc issuance commands first:
|
||||
|
||||
```shell
|
||||
systemctl status step-ca.service
|
||||
systemctl status step-ssh-host-renew.service
|
||||
systemctl status step-ssh-host-renew.timer
|
||||
systemctl status mtls-bootstrap.service
|
||||
ssh-host-cert-check
|
||||
mtls-check
|
||||
```
|
||||
|
||||
`step-ssh-host-renew.service` issues the SSH host certificate when it is missing
|
||||
or expiring. `mtls-bootstrap.service` issues the first Janus mTLS bundle
|
||||
only when the configured certificate files are absent or invalid; recurring mTLS
|
||||
renewal remains handled by `mtls-renew.timer`.
|
||||
|
||||
## Back up the root CA key offline
|
||||
|
||||
|
||||
@@ -16,9 +16,20 @@ in
|
||||
nixos.docker
|
||||
nixos.login-text
|
||||
nixos.mtls
|
||||
({ lib, pkgs, ... }: {
|
||||
({ config, lib, pkgs, ... }: {
|
||||
networking.hostName = hostname;
|
||||
loginText.extraServiceStatus = {
|
||||
"Step-CA" = "step-ca";
|
||||
};
|
||||
sops.defaultSopsFile = ./secrets.yaml;
|
||||
sops.secrets."janus/ssh_host_ed25519_key" = {
|
||||
sopsFile = ./secrets.yaml;
|
||||
owner = "root";
|
||||
group = "root";
|
||||
mode = "0600";
|
||||
path = "/etc/ssh/ssh_host_ed25519_key";
|
||||
restartUnits = [ "sshd.service" "step-ssh-host-renew.service" ];
|
||||
};
|
||||
step-ssh-host = {
|
||||
hostname = hostname;
|
||||
extraPrincipals = [
|
||||
@@ -49,6 +60,12 @@ in
|
||||
"${hostname}.john-stream.com"
|
||||
"192.168.1.244"
|
||||
];
|
||||
bootstrap = {
|
||||
enable = true;
|
||||
after = [ "network-online.target" "sops-nix.service" "step-ca.service" ];
|
||||
wants = [ "network-online.target" "step-ca.service" ];
|
||||
provisionerPasswordFile = config.sops.secrets."janus/admin_jwk".path;
|
||||
};
|
||||
};
|
||||
|
||||
users.users."${username}" = {
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
janus:
|
||||
ca_password: ENC[AES256_GCM,data:GmuqoePDJd5Cn7+sWbvXPlGoUf3SVgYnHOoq9mnPWNWj,iv:VY/8olA+yu66wW+RbhD58GtA0YUWuAtm1HUXtNIIuLk=,tag:fCz3x6B4UPw+XpJE0F3t/g==,type:str]
|
||||
intermediate_ca_crt: ENC[AES256_GCM,data:9W6QXGlcPdfas1ea6S3w1OYI/SeF6YDIdF5J7Hv4ype3,iv:H8oVfbMLBRvU0ywovVSB84g0q1wPhHEdzfpIVAY0Bkg=,tag:7/pU0N1SlfC1i8H2IgPGVA==,type:str]
|
||||
intermediate_ca_key: ENC[AES256_GCM,data:wrzE8pgSZrtMA2jyLuVhMFxr47ICDBUkqOFyFJex9h3g,iv:I3Sz07e+cFDOUunCu7ZGcAjckRJDy3NmTXoB0vtA0HI=,tag:inANSVzyAzWlX4J5pheY8A==,type:str]
|
||||
ssh_host_ca_key: ENC[AES256_GCM,data:IT3PcnbrexRDvxoVEyyxYBx7+Brm3SzVP3Yr2UGkFjeH,iv:edfMiRR+EVx9veTH+mEAFtEdC/rlW8uU5jMD8UX2Ylo=,tag:KKS/2GQE8V6obbjfMLspug==,type:str]
|
||||
ssh_user_ca_key: ENC[AES256_GCM,data:w3gc97CYUODNxk0gIyghd9PCksDVvrdvl8j/NQe/tCtw,iv:WbWlVCPV7niVHEJ9sVcO9SAcLSfquE/bvcR5KClxszk=,tag:08uj+lv9MflYOtQh28K6pw==,type:str]
|
||||
admin_provisioner_encrypted_key: ENC[AES256_GCM,data:/EdlRiPn/LHpQk5sA9VVYTz/U4i6Ag==,iv:r6Rm38L1R4xsnyR97e3J2FdH4KWYewZdADosv9WLAXw=,tag:MnWq9lI7GAXN0FmGTa9zmQ==,type:str]
|
||||
ca_password: ENC[AES256_GCM,data:z6i0ELrn6XwVbJ7gP1GkpWe0Xw==,iv:3pZs/kzPp6bL8iv8jITZnAHuCycbSjhWswgzuJuLvjE=,tag:3gUftdyVXPVWe5dGxh9dvw==,type:str]
|
||||
intermediate_ca_key: ENC[AES256_GCM,data:0QVk/coqb4xvf2vVhZS9wpo9qrGIcdwPRuHqORyJlUIC0VAwH8AoZNKM54uarwag7Wpb+ltv6vhi4frcp+RzGOCDQET28pSrgXBKw5B31vA4uNI6z8pbZbsUrIbVuQDCw/QAu2v+xuCyMyYK58r4hGaCR6RZu6LCZ9Gx7JgmBZWQcZ3Li5jxG+Cm0HM9AympBhCcIGyWfRXreTZkGlzOhknO/MsBhjTmEwpNi4XZdTlsh1dF2tJ3l9nd7B260yAAwcI8AFyCFSIxAZOxYiYWwqUOQu/tFnDA3Zl7Q2LCwCjrr4UyYd+f33R43BWJPD/8TkPOeoyC4RHGIcB11UyyPXGm6TISJITYd0Ii0t8jPkPPFT3bwx5BEadI+/ICYte8bb5d5uV0gbAZB1EpWmeOcEVOOIt076dr6Q==,iv:1ygRHwQfyAAhG7dmm6aB2b5VVDv6JY0/SJGyVVoVSiM=,tag:GTeWsVEE912GXXNjkK2dHw==,type:str]
|
||||
ssh_host_ca_key: ENC[AES256_GCM,data:SA46pzoWXdCLRWPZEHS+n93rspAZ7OQEZTfGF8zNXtuP//522eEocq0CGmmi8AJdST0oWnwbx1EFTyDlz2T8/1eP6ucfNigY8BicLymfdMb57uqJs7lAqN3xakdiM+SqfQcEuGy+SjJjqTjfHw7eqUcFXAuqFcTu6roF+kbVQwqmJyFDhh844aB+YhwIjphBYStqYu6CXGrE4UZBdHtyTHT514I0C1FRXBB6T8QzHr006hh+F7TCCnjVEd+JFNVD9dbf+zfWSdmi5edVRO8/fOrsP0PlDz7LQLocc3YJO+SiLC0m47ihszJJFhE/rYdVfPiagw9L6OmXi2RfDOF1/w2+r2d7Ki9WcyHItnzNsb1FEOFvVICT304s/KbGkesQLpp2JDzyPV3xOIxo/mmSsrJ3kKCahJpxdQ==,iv:rl/Vms9w/8NzaNkSU8kgk2SRV/Ic0DYhdtgT1YFoeEw=,tag:H1GrRqh0CmPN+EfS7+zb3w==,type:str]
|
||||
ssh_user_ca_key: ENC[AES256_GCM,data:jyMrso0G2KVfvn1/Qdl+A0dcQU9/CIrbj1F6hbPB3KWROiQghe/ytTdIsAgKYb+0xUfH27tVjufLY6zEqviPxczzfN9y0ldk+caxH96VEy7da4UdR9rJa7Rl1bvcobRcVIXUCK7rA1deHtJr2wq0KspK4id2QON3w6ggXvYRCe0IAo16p0eGQUMotHqMO+M0qWRkV8IqMKObmxZePK6SGNUEleUNXfSbL4mbN50MIZt9lVKOxOVNZK1BjETtei6IjeeFhPcitJFRjbQTwFB6+5NYqLhEDEZyO3vMZe3poEFtPw9TFlhtYIwtWlBhl+nbh3mAbDWDsHcTix5AL2VVPuDoNpnwCCE4OFzcXjBlBZr28x40o+ukrCxityCXf8M99K3NJVyqgVMdNzjcmViM6pc6wV20t8t/mQ==,iv:RLv3ltoa8Hl1x5Q5xD8xKvlluGd6VTTwxaiV8KhKJBk=,tag:owD14446JnZWznpY9cG2BA==,type:str]
|
||||
admin_provisioner_encrypted_key: ENC[AES256_GCM,data:lv85vgW77Qiy4UOcHt5SaVvuGmBsDIdjTxbUUaxGeP2H+XghTVuHZ/oLNXaPYqBFKKFqctG7FeL3/Gnm5wD7DXn9z6Cwc9eSZjNHUEiYcQLThBHDUOArk/CHhWvuAhWbuT5yBwVJkUmBpO5ZR3slRAh8RS/EBhb3iOXGW63i2Bj1kFAyf4XucmWY8lkRRpIj11gkWA1gVZqJkQI5AJDf4ikvYI/5p33wtQFpClsKJsJRzaoCee+m3w2Obq8hYMdS1mGnXm1L3tbkAj0130dl4/eOvOZq7le2S5Fo0WAFFyk1nhNz771fRI3WtiA85IHC9Nd3v6jGek+PVclk0D4rfvlQTF1yOpemtcsX2BbAbbB2j7sJ5a5bM9O5lotBoFlGQP/co5EU7Ki0+sTYdQxsHVSQhvsCTv5IhSMSjrzkUVN0RV7EwSkKdLK65Q8bR1pmOdXVDAPsyjB4tUJlwWKisrkAc5oyM+mI0Lalz7nvbTYvgtiS0vcBnGmRwcxDC5q//BTm4RhAB2e/My0MadVxF3aEn6fMaoK+3ef1zAToBRG8bV1vC+trhz3ucPqvMNPxjrSQpOdxzJyqlPgfKFVRI/DEQhO0XWpnU3h7wA1mV/dyjeosFVydfKN5iOLl+xF3VgRpisV2oE0Fgas6JkOGwy/d+MpkgNz/Amr+9phZA/bXl2lpDYIKj6aBduqizI4jV1jpM2FHtv99Jkg24DKIcPSZVvzVYr6j8FMmgaxOsA3Z72GlleLdwBKtuBC0GvU=,iv:kcJEhE1d4vYLHk+W2ay2rid+KCuarAIxUZSl/0wi5W4=,tag:7VARgV4a1nT+beIYWelqyw==,type:str]
|
||||
ssh_host_ed25519_key: ENC[AES256_GCM,data:HUL5aKHKXLet6Z1cZKsEgesjT/MoiF2y1MIIdD5YzvUyBBCUuoeZuEPs5TnQwthVITHFsiQP0CJQARLGX2QM0+GpMsw/C7pZYbkuBgVbo0U8qA9fixga3wqqk/2b8bgage8cwjCxg/WCLnfa97b7eeQabSU451VYk7nClLoR1Vh0BSGde0oaXMv0eYmgGPMJaD0JMCZX9wAgX2+VcW2MNP80vsyOJNp/GvkV+9RWa1h3DUh/bpywk3rFm4ftTCcpHjwXNyhXOXWXkheJtKWHAWkPSVzvygODVy45ZZqXke7mOsPZfANy4O2IILic6MLaU/TDQ7FbrNAKq9RPHLAMR8C7XFq7QV2MoLkpZ3fzJOn6tLzfgaGmvHBIekywsHJMgCv3MH1l1Uvw2LYY31dFN6z41I7Mj/bFZGZ4TWDQchuJSAF56aosXSw9DCiTY7gly13HkXNYbTrdKM3BjA5TNyYtJ+KMipket4hBUDAVVQ+fBbk087RFmcuzBSAuCnpkoRvPv767/ht8Q5YQpTY=,iv:m1ZKNB5Ab7pKywHQvxqElkCHokNZUZYbxfXXRF8bDD0=,tag:NPA7ErsJMMmff9XYHrUqbQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCTEFTSy96QTluaWo0bURH
|
||||
YlpFSWx3VnYvSHpDYU1PNmZrb0FuVHVTcG00CmJnT09OZEJad01OZk8zUzlPT1Ft
|
||||
bVpHeW9UMXZYN1JJQVgrTk9OamhZeXcKLS0tIFhXMVNDQjhVcUp2aHhjenlGVW9z
|
||||
cTNPalJOQVJNQWxzMnVMT1lYQS9SNW8KWapdX8dwxEvcqhKI8RJmCWRrV7sRmS72
|
||||
sUt6HaUGpQfUQ97MtS01DYaSZjbAvj8t//mXhLubZddDTtTuhbpjAg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCdFNwM2JQSVlFZXpJZ0VQ
|
||||
eDFYek1TMnNTM3lLbjh4SG56VDl6MGsvb1drCnpLbGZ1N2FwSmRacitFVDJxaFlD
|
||||
Vk5XeCtxRGM0OGV2UFNwQmlFV1pLS1EKLS0tIHB5bm1iRG9WWmpjTlBKcWkwWktz
|
||||
SWVQOVlJU3RBejdpaGpyTTAzVmh0MFkK6nLey1QwSw5J8WeDdUIjJo62pylqOKmP
|
||||
+GANs3OQkRuwW2+nwG+LJR2pfUS6O6PDOz7ZWGKih90/Bk7EMtjOIQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1n94nkvmzezdj08t4dpwukd7s5k4cx2rldwvx9x5qhx3xhqm48yhq6ejldt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyeE04L0RvYmlnaWpxUWhm
|
||||
M01vQVc2bFdNZ2k4TjJBOWZDTlV1V2VkVGd3CnhseTlOZWtqbmpldXUrWnRxSWdR
|
||||
dFFPTFZtSlZjZVdKMDdYQkVJeG5wZUUKLS0tIGxRbEE3ZjNrUTlLSDMrVVIzbnJ5
|
||||
cEFnNmErTlhQMEtZM282STVRUGl0dVkKDyyTGVFDm6u+cdGu/PzFUFgj6ewCf0fi
|
||||
vFhmYA8bTGNg6loThbYxm4uN1u+mJgwOKJ3FRR+u9yYEF6xvqrIbdw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWcnVWS2lpMDM5VWxLWDIx
|
||||
R0xWQ3ZOaEVFNTR5bXArNTc4MzFOM2xCQWhjCnBZeUtJQ3RvWDdZeVRpRzBPbTl2
|
||||
M3FOTlRnTDhIaGg2VzM3MFVEc1FVUmcKLS0tIDAzeDlaZGt5Q2dJbzNWRVB4Qmwr
|
||||
SWpxcytNdEFBQ2o0Q3lWSHZSNEdCUUEKgHE0j678WIhcQQsZQ5RcQNkQcP++ibvr
|
||||
ZT3ScL4PAYhH+lxciJK4tit53taevbp5o0jFibaup4ByDvgj7HPclw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjeHQ4aDlSU2M2bzE1ODFY
|
||||
Ym1NNEFFaVhrVlR0WUs1ZUVwTFVGN3QwMFZJCmdJWWFaeHByUlB2U3FLbEpwRkJI
|
||||
ckRTNWx2dVl3Mi9ZNjhwUFNTdjZIaTAKLS0tIDZ0dFpCa2F3aFRCYmh2N3Nwb2dF
|
||||
ZUJxakM3Wnc3U3JwTklPcllydXZBUXcKlf9C2/Gb9H9PdEKHdAd48dsalm43vYIz
|
||||
ODYoLItstmJvT/rZ+XijZFALNsRrIj5435CW6V34OsfCkFkjeHz17w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
|
||||
lastmodified: "2026-07-04T04:20:22Z"
|
||||
mac: ENC[AES256_GCM,data:N0+lAFghaas9fg9PMcsaMPKJbYmnSrgPIFPsEuC749nzMX1IfQVaTLYm6ziZiq8TpscGR1tBqzw2eXiSZG4XfI44naEzO8CvY87aaZAL19/c/j4XE8eBqv1RLsLMbBIYYs8h6aZXCwhWUa3bwFukjueD0iu2X3md6V4T+4RnYSE=,iv:Xd/DCSDymp+KURHjM3Cht8oqbyN/k5+eybdi3pK5eww=,tag:tmDJoo5RKXQTnVjqkXsZ1w==,type:str]
|
||||
lastmodified: "2026-07-04T16:08:14Z"
|
||||
mac: ENC[AES256_GCM,data:9cWYDRVot45SO79sR6ETDI2zwMW5EVH2k3pOObQ/FWPhkdZYBkox3uf2UXgzCo9An98ta3bmhcZnfv77jAnZOz088s4MWTvl89ViI0SFtPnm94Ml2NopxmKdRndja5Aj5AOD5MUrqofaSXIhOTxekp/goCGknd986zuUa7yVhhw=,iv:38ycphhpGYKHFhWTQeM5QlfzS8cnaC3Y5yAkO/kxfEk=,tag:ILOV+YyP3kA3e9FJmBC5Gw==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.1
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{ inputs, ... }:
|
||||
let
|
||||
userName = "john";
|
||||
sshHostCAPubKey = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ=";
|
||||
sshHostCAPubKeyPath = ../hosts/janus/ssh_host_ca_key.pub;
|
||||
in
|
||||
{
|
||||
flake.modules.nixos.ssh = { config, pkgs, lib, ... }:
|
||||
let
|
||||
cfg = config.ssh;
|
||||
configDir = "/etc/ssh";
|
||||
sshHostCAPubKey = lib.removeSuffix "\n" (builtins.readFile sshHostCAPubKeyPath);
|
||||
in
|
||||
{
|
||||
options.ssh = {
|
||||
@@ -72,6 +73,7 @@ in
|
||||
identityFile = cfg.identityFile;
|
||||
publicKeyFile = "${identityFile}.pub";
|
||||
certificateFile = "${identityFile}-cert.pub";
|
||||
sshHostCAPubKey = lib.removeSuffix "\n" (builtins.readFile sshHostCAPubKeyPath);
|
||||
in
|
||||
{
|
||||
options.ssh = with lib; {
|
||||
|
||||
@@ -11,6 +11,24 @@
|
||||
cfg.hostname
|
||||
"${cfg.hostname}.john-stream.com"
|
||||
] ++ cfg.extraPrincipals);
|
||||
sshHostCertRenew = pkgs.writeShellScriptBin "ssh-host-cert-renew" ''
|
||||
set -euo pipefail
|
||||
|
||||
if [ ! -s "${sshKeyPath}.pub" ]; then
|
||||
${lib.getExe' pkgs.openssh "ssh-keygen"} -y -f "${sshKeyPath}" > "${sshKeyPath}.pub"
|
||||
chmod 0644 "${sshKeyPath}.pub"
|
||||
fi
|
||||
|
||||
${lib.getExe pkgs.step-cli} ssh certificate \
|
||||
--host --sign \
|
||||
--provisioner "${cfg.provisioner}" \
|
||||
--provisioner-password-file "${provisionerPasswordPath}" \
|
||||
${principalArgs} \
|
||||
"${cfg.hostname}" "${sshKeyPath}.pub"
|
||||
'';
|
||||
sshHostCertCheck = pkgs.writeShellScriptBin "ssh-host-cert-check" ''
|
||||
${lib.getExe' pkgs.openssh "ssh-keygen"} -Lf ${sshCertPath}
|
||||
'';
|
||||
in
|
||||
{
|
||||
# NixOS Options
|
||||
@@ -45,17 +63,9 @@
|
||||
};
|
||||
networking.nameservers = [ "192.168.1.150" ];
|
||||
networking.dhcpcd.extraConfig = "nohook resolv.conf";
|
||||
environment.systemPackages = with pkgs; [
|
||||
# step-cli
|
||||
(writeShellScriptBin "ssh-host-cert-renew" ''
|
||||
${lib.getExe pkgs.step-cli} ssh certificate \
|
||||
--host --sign \
|
||||
--provisioner "${cfg.provisioner}" \
|
||||
--provisioner-password-file "${provisionerPasswordPath}" \
|
||||
${principalArgs} \
|
||||
"${cfg.hostname}" "${sshKeyPath}.pub"
|
||||
'')
|
||||
(writeShellScriptBin "ssh-host-cert-check" "${lib.getExe' pkgs.openssh "ssh-keygen"} -Lf ${sshCertPath}")
|
||||
environment.systemPackages = [
|
||||
sshHostCertRenew
|
||||
sshHostCertCheck
|
||||
];
|
||||
|
||||
systemd.services.step-ssh-host-renew = {
|
||||
@@ -63,7 +73,7 @@
|
||||
wantedBy = [ ];
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
path = with pkgs; [ coreutils systemd step-cli openssh ];
|
||||
path = with pkgs; [ coreutils systemd step-cli openssh ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "root";
|
||||
@@ -71,8 +81,14 @@
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
if ${lib.getExe pkgs.step-cli} ssh needs-renewal "${sshCertPath}" --expires-in "4h"; then
|
||||
echo "Renewing SSH host certificate"
|
||||
|
||||
renew=0
|
||||
if [ ! -s "${sshCertPath}" ]; then
|
||||
echo "SSH host cert missing: ${sshCertPath}"
|
||||
renew=1
|
||||
elif ${lib.getExe pkgs.step-cli} ssh needs-renewal "${sshCertPath}" --expires-in "4h"; then
|
||||
echo "SSH host cert needs renewal"
|
||||
renew=1
|
||||
else
|
||||
rc=$?
|
||||
if [ "$rc" -eq 1 ]; then
|
||||
@@ -81,12 +97,17 @@
|
||||
fi
|
||||
|
||||
if [ "$rc" -eq 2 ]; then
|
||||
echo "SSH host cert missing: ${sshCertPath}" >&2
|
||||
exit 1
|
||||
echo "SSH host cert missing or unreadable: ${sshCertPath}"
|
||||
renew=1
|
||||
else
|
||||
echo "step ssh needs-renewal failed with rc=$rc" >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "step ssh needs-renewal failed with rc=$rc" >&2
|
||||
exit "$rc"
|
||||
if [ "$renew" -eq 1 ]; then
|
||||
${lib.getExe sshHostCertRenew}
|
||||
${lib.getExe sshHostCertCheck}
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user