Compare commits

12 Commits
Author SHA1 Message Date
John Lancaster a9b831a6b0 ssh cert consolidation 2026-07-05 00:03:28 -05:00
John Lancaster b1f4b6ad41 janus deployment 2026-07-04 23:19:35 -05:00
John Lancaster f2ba5d914c formatting tweaks 2026-07-04 22:25:48 -05:00
John Lancaster deae5d223a ssh fallback keys during bootstrap 2026-07-04 22:25:28 -05:00
John Lancaster fae21e2962 flake.lock update 2026-07-04 11:52:15 -05:00
John Lancaster 69f492c7cc WIP janus secrets 2026-07-04 11:49:58 -05:00
John Lancaster cddc369687 commiting public keys/certs for janus 2026-07-04 10:38:29 -05:00
John Lancaster 3f767dfc43 sops instructions 2026-07-04 00:30:15 -05:00
John Lancaster dccdc15851 setting nvim as editor 2026-07-03 23:42:27 -05:00
John Lancaster a3a1cd7066 WIP janus secrets 2026-07-03 23:29:20 -05:00
John Lancaster 35fec024e0 created config options for step-ca 2026-07-03 23:06:06 -05:00
John Lancaster 6499ad7612 WIP with placeholder secrets 2026-07-03 22:46:49 -05:00
27 changed files with 924 additions and 318 deletions
@@ -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.ssh-certs` 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 ssh-certs-renew.service`, and `systemctl status ssh-certs-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.
@@ -0,0 +1,72 @@
---
description: "Use when working with SOPS in this repo: editing secrets, adding multiline values, changing recipients, or wiring sops-nix consumers. Captures repo conventions, safe commands, and validation habits."
applyTo: ".sops.yaml, keys/secrets.yaml, modules/hosts/**/secrets.yaml, modules/hosts/**/*.nix, modules/programs/sops.nix, modules/features/**/*.nix, modules/services/**/*.nix"
---
# Using SOPS In This Repo
This repo treats SOPS files as the source of encrypted runtime material, and Nix code as the wiring that exposes those secrets to services. Keep those roles separate: edit secret values with SOPS, consume them through sops-nix paths, and keep recipient policy scoped to the machines or operators that actually need access.
## Mental Model
- Secrets live in encrypted YAML files; Nix modules should refer to secret paths, not decrypted values.
- Host-only credentials belong in `modules/hosts/<host>/secrets.yaml`.
- Shared credentials belong in `keys/secrets.yaml` only when multiple hosts intentionally consume the same material.
- `.sops.yaml` controls who can decrypt each file; keep those recipient sets narrow and explicit.
- Secret key names are an interface. Rename them only when every consumer changes in the same edit.
## Editing Secrets
- Use SOPS tooling for all value changes: `sops <file>`, `edit-secrets`, or `sops set ...`.
- Do not hand-edit `ENC[...]` payloads. That bypasses SOPS and breaks the encrypted document's integrity metadata.
- Keep plaintext out of tracked files. Temporary plaintext files are acceptable only as local working inputs and should be removed after use.
- Prefer `SOPS_EDITOR=nvim sops <file>` for interactive edits when editor choice matters.
For large or generated values, avoid putting the secret directly in shell history:
```bash
sops set --value-file <secrets-file> '["parent"]["key"]' /path/to/plaintext-value
```
If using inline `sops set`, the value argument must be valid JSON.
## Multiline Values
Use YAML literal blocks for private keys, certificates, provisioner keys, and other values where newlines matter:
```yaml
janus:
ssh_user_ca_key: |-
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
```
- Use `|-` when the final trailing newline should be stripped.
- Use `|` when the consuming program expects the final trailing newline.
- Avoid folded style (`>`) for keys and certs because it rewrites line breaks.
## Wiring Secrets Into Nix
- Consume secrets through `config.sops.secrets."<key>".path` or `config.sops.placeholder` where templating requires it.
- Do not read decrypted secret contents during Nix evaluation.
- Set `owner`, `group`, and `mode` explicitly for non-root services.
- Use `path` or `sops.templates` when a service needs a specific file layout instead of copying secret contents into the store or tracked files.
- If moving a secret between shared and host-local files, update `sops.defaultSopsFile`, `mysops.hostSecretFile`, and every affected `sops.secrets` declaration together.
## Recipient And Rotation Practices
- Add dedicated `.sops.yaml` `path_regex` rules for host-local files before broad fallback rules.
- Keep broad fallback rules conservative; do not expand them to grant casual access to every YAML or JSON file.
- Run `sops updatekeys <file>` after changing recipients so data is rewrapped for the new key set.
- Rotate or migrate in order: update recipients, re-encrypt affected files, update consumers, then validate affected hosts.
## Validation Habits
After SOPS or sops-nix changes, check the contract rather than only the syntax:
1. The expected secrets file is still selected by `sops.defaultSopsFile` or `mysops.hostSecretFile`.
2. `.sops.yaml` has a specific rule for any host-local `secrets.yaml` touched.
3. Secret names in YAML still match the `sops.secrets."<key>"` declarations.
4. Services that need restart or reload behavior have `restartUnits` or `reloadUnits` set.
5. Affected host evaluation/build catches missing secret keys before deployment.
@@ -19,31 +19,31 @@ Use this instruction when changing Soteria host wiring in `modules/hosts/soteria
When editing `modules/hosts/soteria/default.nix`, preserve this composition unless the task explicitly changes architecture: When editing `modules/hosts/soteria/default.nix`, preserve this composition unless the task explicitly changes architecture:
- Import `nixos.step-ssh-host` to enable host SSH cert integration. - Import `nixos.ssh-certs` to enable host SSH cert integration.
- Import `nixos.janus-ca` so Step client trust material and defaults remain available. - Import `nixos.janus-ca` so Step client trust material and defaults remain available.
- Import `nixos.restic-server` so the RESTic REST server implementation lives in the shared module rather than inline host wiring. - Import `nixos.restic-server` so the RESTic REST server implementation lives in the shared module rather than inline host wiring.
- Keep `step-ssh-host.hostname = hostname;` so cert principals match host naming. - Keep `ssh-certs.hostname = hostname;` so cert principals match host naming.
## SSH Cert Mechanics On Soteria ## SSH Cert Mechanics On Soteria
Soteria SSH certificate behavior is the result of multiple modules. Keep this flow intact: Soteria SSH certificate behavior is the result of multiple modules. Keep this flow intact:
1. `soteria/default.nix` imports `nixos.step-ssh-host` and sets `step-ssh-host.hostname = "soteria"` (via `hostname`). 1. `soteria/default.nix` imports `nixos.ssh-certs` and sets `ssh-certs.hostname = "soteria"` (via `hostname`).
2. `modules/services/step-ca/ssh-host.nix`: 2. `modules/services/step-ca/ssh-host.nix`:
- Enables `ssh.certificates.enable = true`. - Enables `ssh.certificates.enable = true`.
- Requires `sops.secrets."janus/admin_jwk"` (provisioner credential) from Soteria's `secrets.yaml`. - Requires `sops.secrets."janus/admin_jwk"` (provisioner credential) from Soteria's `secrets.yaml`.
- Defines cert paths at `/etc/ssh/ssh_host_ed25519_key` and `/etc/ssh/ssh_host_ed25519_key-cert.pub`. - Defines cert paths at `/etc/ssh/ssh_host_ed25519_key` and `/etc/ssh/ssh_host_ed25519_key-cert.pub`.
- Exposes `ssh-host-cert-renew` and `ssh-host-cert-check` helper binaries. - Exposes `ssh-host-cert-renew` and `ssh-host-cert-check` helper binaries.
- Schedules `step-ssh-host-renew.timer` every 4h with jitter. - Schedules `ssh-certs-renew.timer` every 4h with jitter.
3. `modules/services/ssh.nix` consumes `ssh.certificates.enable` and configures OpenSSH to: 3. `modules/services/ssh.nix` consumes `ssh.certificates.enable` and configures OpenSSH to:
- Set `TrustedUserCAKeys = /etc/ssh/ssh_user_ca.pub`. - Set `TrustedUserCAKeys = /etc/ssh/ssh_user_ca_key.pub`.
- Set `HostCertificate = /etc/ssh/ssh_host_ed25519_key-cert.pub`. - Set `HostCertificate = /etc/ssh/ssh_host_ed25519_key-cert.pub`.
- Install the trusted user CA file into `/etc/ssh/ssh_user_ca.pub`. - Install the trusted user CA file into `/etc/ssh/ssh_user_ca_key.pub`.
4. `nixos.janus-ca` provides Step CA trust bootstrapping (`/etc/step-ca/defaults.json` and linked root CA material), allowing Step CLI operations to trust and reach the CA endpoint. 4. `nixos.janus-ca` provides Step CA trust bootstrapping (`/etc/step-ca/defaults.json` and linked root CA material), allowing Step CLI operations to trust and reach the CA endpoint.
Current implementation note: Current implementation note:
- `step-ssh-host-renew.service` currently checks renewal state via `step ssh needs-renewal` and logs status. It does not directly invoke `ssh-host-cert-renew` in the service script. Preserve this behavior unless the task explicitly asks to change renewal execution semantics. - `ssh-certs-renew.service` currently checks renewal state via `step ssh needs-renewal` and logs status. It does not directly invoke `ssh-host-cert-renew` in the service script. Preserve this behavior unless the task explicitly asks to change renewal execution semantics.
## SOPS/Secrets Mechanics On Soteria ## SOPS/Secrets Mechanics On Soteria
@@ -135,5 +135,5 @@ Current implementation note:
- Do not rename the secret key `janus/admin_jwk` without updating all consumers. - Do not rename the secret key `janus/admin_jwk` without updating all consumers.
- Do not change SSH host key/cert filenames unless OpenSSH `HostKey`/`HostCertificate` paths are updated together. - Do not change SSH host key/cert filenames unless OpenSSH `HostKey`/`HostCertificate` paths are updated together.
- If changing hostnames or domains, update cert principals in `step-ssh-host` and corresponding SSH client targets together. - If changing hostnames or domains, update cert principals in `ssh-certs` and corresponding SSH client targets together.
- If adding or removing `nixos.step-ssh-host`, explain impact on SSH cert issuance, renewal, and trust in the change summary. - If adding or removing `nixos.ssh-certs`, explain impact on SSH cert issuance, renewal, and trust in the change summary.
@@ -12,8 +12,8 @@ Use this instruction when changing the shared NixOS module in modules/services/s
- Keep this module reusable across hosts; do not hard-code host-local file paths beyond SSH system paths under /etc/ssh. - Keep this module reusable across hosts; do not hard-code host-local file paths beyond SSH system paths under /etc/ssh.
- Preserve the option interface unless the task explicitly changes it: - Preserve the option interface unless the task explicitly changes it:
- step-ssh-host.hostname (required host identity used for cert principals) - ssh-certs.hostname (required host identity used for cert principals)
- step-ssh-host.provisioner (default: "admin") - ssh-certs.provisioner (default: "admin")
- Keep imports = [ inputs.self.modules.nixos.ssh ] so OpenSSH host certificate settings stay composed through the shared SSH module. - Keep imports = [ inputs.self.modules.nixos.ssh ] so OpenSSH host certificate settings stay composed through the shared SSH module.
## Secret Key Contract ## Secret Key Contract
@@ -29,7 +29,7 @@ When this module is enabled by a host, the host is expected to provide:
- A working Step trust/bootstrap path (for example via janus-ca or equivalent trust material). - A working Step trust/bootstrap path (for example via janus-ca or equivalent trust material).
- `sops.defaultSopsFile` containing `janus.admin_jwk` (or equivalent if contract is intentionally changed everywhere). - `sops.defaultSopsFile` containing `janus.admin_jwk` (or equivalent if contract is intentionally changed everywhere).
- `step-ssh-host.hostname` matching deployed host identity and DNS naming expectations. - `ssh-certs.hostname` matching deployed host identity and DNS naming expectations.
## SSH Certificate Paths And Principals ## SSH Certificate Paths And Principals
+1
View File
@@ -1,2 +1,3 @@
result/ result/
.devenv/ .devenv/
*.gz
+15
View File
@@ -3,7 +3,22 @@ keys:
- &john-pc age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt - &john-pc age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
- &test-nix age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0 - &test-nix age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
- &soteria age1h0prahyukq4l564yqwgcpg3g6gdrjflk0suklussjjrjstxd9uesws8633 - &soteria age1h0prahyukq4l564yqwgcpg3g6gdrjflk0suklussjjrjstxd9uesws8633
- &janus age1qahhlzeanprtykym9jymk2t95uedr7cwx9sdshx46q2m6u66fucsqua8l3
creation_rules: creation_rules:
- path_regex: modules/hosts/janus/secrets\.yaml$
key_groups:
- age:
- *janus
- *john-pc
- *test-nix
- path_regex: keys/secrets\.yaml$
key_groups:
- age:
- *john-p14s
- *john-pc
- *test-nix
- *soteria
- *janus
- path_regex: soteria/secrets\.yaml$ - path_regex: soteria/secrets\.yaml$
key_groups: key_groups:
- age: - age:
Generated
+16 -16
View File
@@ -38,11 +38,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1778716662, "lastModified": 1782949081,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -95,11 +95,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1782881387, "lastModified": 1783134515,
"narHash": "sha256-HvlS1KDGXDjd1bNzzPvH1mkDJATlDAfVYfTh//wJBEA=", "narHash": "sha256-qMoZazubXlXUD9k/syJ/aiWC4X4g73mwVmZ7Z4+rdpM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "d32537981c036473cf6990ae03176a5d84c43b29", "rev": "b885baad531fa3d3beae2ba9a0712d22974d8016",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -193,11 +193,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1782821651, "lastModified": 1782948114,
"narHash": "sha256-OBddwHNJ+qxOHaKih0bE2PXJeZHlzUC8WOHZrUcnFwY=", "narHash": "sha256-VGtEDss4T0hPTnlWiiaa29y/lXM9SxuSMVtzPYf3wS0=",
"rev": "e52c192be9d7b2c4bd4aed326c8731b35f8bb75c", "rev": "9e92285f211dad236540fd617d7e30e0b99bc0e1",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1025128.e52c192be9d7/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1026211.9e92285f211d/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -215,11 +215,11 @@
"systems": "systems_2" "systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1782841360, "lastModified": 1783164413,
"narHash": "sha256-S1f/S5Vv7wURVUtzPkmAFlKPVciaPLXxoeg7/GCnl28=", "narHash": "sha256-7DMamcvS35/tI+KZKXt45bHAq2Vnucl74Xetvp7DR3I=",
"owner": "notashelf", "owner": "notashelf",
"repo": "nvf", "repo": "nvf",
"rev": "6fad9e9a998dd3a7783c839ce2928877d3e5a883", "rev": "c8386ccfbc7053d454fa2d12d716c6ebb4167d51",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -250,11 +250,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1782165805, "lastModified": 1783174389,
"narHash": "sha256-478kKQBvK6SYTOdN2h9jhKJv94nbXRbFMfuL1WshErg=", "narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "56b24064fdcaedca53553b1a6d607fd23b613a24", "rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9",
"type": "github" "type": "github"
}, },
"original": { "original": {
+39 -21
View File
@@ -9,33 +9,51 @@ api:
gmail_client_secret: ENC[AES256_GCM,data:du2gEY5TQIwpUEvJKDWKY3noLRGeiKek4IMwPUusVx8NMys=,iv:hIYi1xQYf6+hDhK0pNprBYu6wXwRH2yOTwQg6pzQa0A=,tag:sqmQ5GCkKbHpIy2R+Y5G/A==,type:str] gmail_client_secret: ENC[AES256_GCM,data:du2gEY5TQIwpUEvJKDWKY3noLRGeiKek4IMwPUusVx8NMys=,iv:hIYi1xQYf6+hDhK0pNprBYu6wXwRH2yOTwQg6pzQa0A=,tag:sqmQ5GCkKbHpIy2R+Y5G/A==,type:str]
sops: sops:
age: age:
- recipient: age1f6drjusg866yscj8029tk4yfpgecklrvezldm02ankm6h8nnwu5s2u6ahy - enc: |
enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0UEpja2kxdThZVWZhOGVP YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwWlpYV1o3SVdJdmJ5TWM3
S0NtSi84MjhnN0RORkh2NjZ4YlYvWS9kZDBNClFzYnVxWnhmQkpCRkRFVUx1RDdX Qmg4WVkvL21aQ2wvMUcxeGIxL2FQRUNOZFhZCkFqT3RtNTZvODJZczVwcEZkMzBp
ZHFqYXRqYXM0cWJzcU5EeEtSR1BUVzAKLS0tIDdEY2pnVTJqWlNZVkZldXVYVmFH eFFTVmJLb1ZOQVVVSWl4OVNQWkM4VWcKLS0tIEV4WmhEOGpkUWFBTCtqaldxdXFO
dVNBRUVodU5sRnpVcG1GZ1RiZzhjTXMKefqBvvD/qZwcSHmFjUnleukVRLueG36Y T20rVXNrY0ZBWlBaS0lWdGIrYUFNdm8KL06MsEu0fy6gKX5khZoxaypKDgEEJnTL
Q81KlwQweF2F8kHl7Bqsi+3hH1dZZbVm3vjuGpWFOoti7fowUV55Kw== hCh4sCmTC3l7vQLv6deWf1Xhc8vSYhhNaWFCZ+KSaUen+6Bm/WrRIg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt recipient: age1f6drjusg866yscj8029tk4yfpgecklrvezldm02ankm6h8nnwu5s2u6ahy
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqZFZxbDhVUWFEUGhPMlZI YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwRGVJTXJtSk9KUFpCdksv
SFdBYkpxSnAxTUZXbjVwQnlZQ3l1SWtuZGg0CmVBdnVHbTNUcmwvK01iMnZKZTJh SUJ4YXBSTndhZkVHNlV2TTlyS0MrQXdqOG1jCkk0c3p0ZUlHMzZmR0FTUTMxOTl1
ajFla3kzYUl4ZWY3czA0WUdNM2lpVFUKLS0tIHo5Uk1pV296MXdnUTZGQ25haWZG c0dFTW9yZFpuSUdwVUs0dlJiL2d2cFUKLS0tIE1VWldEYmR6cWFoRTkycmllVnpt
QWZDWGRaRDBhY1ZkZk5oTHY0ZVV2RXMKanv+WWRhf5nl+aw/T6QZFVQQmhV1DZfB TGYxU2J0NzAvMjBMOXVMZEJTYTNkamMK2zfuTeHJ/8I07oNhp3MN1wKlmtVpahke
jkSzOAKOgPx7toYFmpq9E8fAH+zrMzDbxI2z2uyrOFI6v+QE0Ul/iQ== cz8FVesp/+53HxfQAdnC7l/Cv7P71aVEyr2/oTEd4gt9HsHVVw3O6A==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0 recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZcGtMNi9RSG5aTVV5cWdT YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwR3VEWmZ4REpUN0M5NTRT
NittUXN0Qnpld1YvOU50OUh3Z3ZiSzhHOHdNCnc4TmdYbS9QQnBLbldHSytIdkJl U1l1S0k1MnFrYnhRanB4UStOQU0vYXE0TjFZCld2cDBlRXNyRlJWMlZ4TENadURy
R0psQWxkZTgyZTRzckkrTGpyNCsvR2sKLS0tIEdLb05aT2I2S3BKcFRrVmtvTGw5 cUN0RmpYYld3WlY0TzkyNXlqbFVnKzgKLS0tIHhtSG5VU3RMclMyZFJmbk1DMkV6
Z1orRCtkTDVXSktuck5pTmV4K05qZHMKZlHHu07q+GnyDDgdwW2Ic3P23PmoSPwn elRhVTV6Tk5OTjUybldvUXY3Sk9hdkEKg17Pagk12Pvj6cNr49HN8tImM4w7pnkc
WuNLZdlZQleROaRb+zpD+9P1HGGJ3mWAlNlnmjGrRk453k1PbBQ5Og== XLCWIHUCe3QpJlgs4USXbyA06PmsNl7PVvCoxmf6tPegt+SFMTuDRQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMUUtJWklHTGpLYTJuMkZi
N2YzMlBUU3dxUGZwSUJjRktVYmp4RlowZ0c0CmdWdmFOQndpQWM0a0VOZUtlckhz
SktRclh5ZFZLaCt2RjhIdGU1aUs0c2sKLS0tIGI4UGFUcHhPVis5Z3dhanhiMGwv
OXRWQnNKcU9XRVJRWWdUNTZyZUdETzAKSchzGq3teDdBiyzJEujlxPaoJsVww4JX
3VvsUZLxnV1yWu83X1bp6uZA3YLMdQgPcHezdgmIyixp+ATHh9fgzw==
-----END AGE ENCRYPTED FILE-----
recipient: age1h0prahyukq4l564yqwgcpg3g6gdrjflk0suklussjjrjstxd9uesws8633
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtOHFwemUyc2pEKzlibVBo
MlI4R0VTMHpwK21MaXZkSUp5dnIwUnVZOFRvCjVsM3FrclFJL0s2a1RwU1F2VXNF
MnRBOVUydFdEWXY3Mk9oTGw5Z05Zb28KLS0tIE9QRFdCVGR1Z0UyeE5KSzgrWnpP
Q25nZWx3bFdlek1OSjJsSk1QREdGRU0K++hDWzoKbBkGkMAkruuEwQKa1Jrc8POs
liN274cEdi1T6nmwBJ05w0alUR2XuYEn9HNKdvz8EntNuKIhCpHing==
-----END AGE ENCRYPTED FILE-----
recipient: age1qahhlzeanprtykym9jymk2t95uedr7cwx9sdshx46q2m6u66fucsqua8l3
lastmodified: "2026-03-15T15:06:29Z" 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] 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 unencrypted_suffix: _unencrypted
+59
View File
@@ -144,6 +144,33 @@ in
type = lib.types.str; type = lib.types.str;
default = "/etc/step-ca/certs"; 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 { certReaders = lib.mkOption {
description = ""; description = "";
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
@@ -179,6 +206,38 @@ in
mtlsRenewWrapper.outputs.systemd-system 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 { systemd.timers.mtls-renew = lib.mkIf cfg.renew.enable {
wantedBy = [ "timers.target" ]; wantedBy = [ "timers.target" ];
timerConfig = { timerConfig = {
+136 -19
View File
@@ -1,29 +1,146 @@
{ self, inputs, ... }: { { self, inputs, ... }:
flake.modules.homeManager.step-client = { config, pkgs, lib, ... }: { let
home.file.".step/config/defaults.json".text = builtins.toJSON { defaultCaUrl = "https://janus.john-stream.com/";
ca-url = "https://janus.john-stream.com/"; defaultFingerprint = "2036c44f7b5901566ff7611ea6c927291ecc6d2dd00779c0eead70ec77fa10d6";
fingerprint = "2036c44f7b5901566ff7611ea6c927291ecc6d2dd00779c0eead70ec77fa10d6"; defaultRoot = ../hosts/janus/root_ca.crt;
root = ../hosts/janus/root_ca.crt;
mkStepBootstrap = { pkgs, caUrl, fingerprint, install ? false }:
(inputs.self.wrappers.stepBootstrap.apply {
inherit pkgs install;
ca-url = caUrl;
inherit fingerprint;
}).wrapper;
mkDefaultsText = cfg: builtins.toJSON {
ca-url = cfg.caUrl;
fingerprint = cfg.fingerprint;
root = cfg.root;
}; };
home.packages = [ in
inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.step-bootstrap {
flake.modules.nixos.step-client = { config, pkgs, lib, ... }:
let
cfg = config."step-client";
in
{
options."step-client" = {
enable = lib.mkOption {
description = "Enable step-ca client bootstrap";
type = lib.types.bool;
default = true;
};
caUrl = lib.mkOption {
description = "The step-ca URL used for bootstrap and renewal.";
type = lib.types.str;
default = defaultCaUrl;
};
fingerprint = lib.mkOption {
description = "The SHA256 fingerprint of the step-ca root certificate.";
type = lib.types.str;
default = defaultFingerprint;
};
root = lib.mkOption {
description = "The step-ca root certificate used for bootstrap.";
type = lib.types.path;
default = defaultRoot;
};
certDir = lib.mkOption {
description = "Directory used to store root CA material for mTLS and step bootstrap.";
type = lib.types.str;
default = "/etc/step-ca/certs";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
(mkStepBootstrap {
inherit pkgs;
caUrl = cfg.caUrl;
fingerprint = cfg.fingerprint;
})
]; ];
# sops.secrets."step-ca-defaults" = {
# sopsFile = ../hosts/janus/defaults.json; environment.etc."step-ca/defaults.json".text = mkDefaultsText cfg;
# format = "json";
# key = ""; # This causes it to decode the whole file systemd.tmpfiles.rules = [
# path = "${config.home.homeDirectory}/defaults.json"; "d ${cfg.certDir} 0750 root root -"
# mode = "0400"; "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 -"
"L+ /root/.step/config/defaults.json - - - - /etc/step-ca/defaults.json"
"L+ /root/.step/certs/root_ca.crt - - - - ${cfg.certDir}/root_ca.crt"
];
};
};
flake.modules.homeManager.step-client = { config, pkgs, lib, ... }:
let
cfg = config."step-client";
in
{
options."step-client" = {
enable = lib.mkOption {
description = "Enable step-ca client bootstrap";
type = lib.types.bool;
default = true;
};
caUrl = lib.mkOption {
description = "The step-ca URL used for bootstrap and renewal.";
type = lib.types.str;
default = defaultCaUrl;
};
fingerprint = lib.mkOption {
description = "The SHA256 fingerprint of the step-ca root certificate.";
type = lib.types.str;
default = defaultFingerprint;
};
root = lib.mkOption {
description = "The step-ca root certificate used for bootstrap.";
type = lib.types.path;
default = defaultRoot;
};
certDir = lib.mkOption {
description = "Directory used to store root CA material for mTLS and step bootstrap.";
type = lib.types.str;
default = "${config.home.homeDirectory}/.step/certs";
};
};
config = lib.mkIf cfg.enable (
let
certDirPath = lib.removePrefix "${config.home.homeDirectory}/" cfg.certDir;
in
{
home.packages = [
(mkStepBootstrap {
inherit pkgs;
caUrl = cfg.caUrl;
fingerprint = cfg.fingerprint;
})
];
home.file.".step/config/defaults.json".text = mkDefaultsText cfg;
home.file."${certDirPath}/root_ca.crt".source = cfg.root;
}
);
}; };
perSystem = { system, pkgs, lib, ... }: { perSystem = { system, pkgs, lib, ... }: {
packages.step-bootstrap = (inputs.self.wrappers.stepBootstrap.apply { packages.step-bootstrap = mkStepBootstrap {
inherit pkgs; inherit pkgs;
ca-url = "https://janus.john-stream.com"; caUrl = defaultCaUrl;
fingerprint = "2036c44f7b5901566ff7611ea6c927291ecc6d2dd00779c0eead70ec77fa10d6"; fingerprint = defaultFingerprint;
install = true; install = true;
}).wrapper; };
}; };
flake.wrappers.stepBootstrap = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: { flake.wrappers.stepBootstrap = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
+85
View File
@@ -0,0 +1,85 @@
# Janus
Generate passwords:
```shell
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 for reference (Janus now uses whatever key
already exists on the target 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
```
Insert generated runtime CA material into `modules/hosts/janus/secrets.yaml` under `janus`:
- `/tmp/janus-step-ca-bootstrap/ca_password.txt` -> `ca_password`
- `/tmp/janus-step-ca-bootstrap/step/secrets/intermediate_ca_key` -> `intermediate_ca_key`
- `/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)
If you are only validating wiring first, `admin_provisioner_encrypted_key` can be an encrypted placeholder and replaced later.
If rotating provisioner password, also set:
- `/tmp/janus-step-ca-bootstrap/admin_jwk_password.txt` -> `janus.admin_jwk` in `keys/secrets.yaml`
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`)
- `keys/secrets.yaml`: shared Janus provisioner secret (`janus.admin_jwk`) consumed by `ssh-certs` across hosts
Then update public artifacts in repo from generated output:
- `modules/hosts/janus/root_ca.crt` from `/tmp/janus-step-ca-bootstrap/step/certs/root_ca.crt`
- `modules/hosts/janus/intermediate_ca.crt` from `/tmp/janus-step-ca-bootstrap/step/certs/intermediate_ca.crt` (public certificate; intentionally committed, not stored in SOPS)
- `modules/hosts/janus/fingerprint` from:
```shell
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 ssh-certs-renew.service
systemctl status ssh-certs-renew.timer
systemctl status mtls-bootstrap.service
ssh-host-cert-check
mtls-check
```
`ssh-certs-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
The root CA private key is **not** deployed and is never needed by the running CA.
It is only used to sign/rotate intermediates. Store it offline (e.g. 1Password)
before wiping the bootstrap directory, otherwise intermediate rotation becomes
impossible and any future rotation forces a full root rotation (redistributing
`root_ca.crt` + `fingerprint` to every client).
Back up, then wipe the bootstrap material:
- `/tmp/janus-step-ca-bootstrap/step/secrets/root_ca_key` -> offline secret store
- `/tmp/janus-step-ca-bootstrap/ca_password.txt` -> offline secret store (root/intermediate key password)
```shell
shred -u /tmp/janus-step-ca-bootstrap/step/secrets/root_ca_key 2>/dev/null || true
rm -rf /tmp/janus-step-ca-bootstrap
```
+39 -54
View File
@@ -2,79 +2,63 @@
let let
username = "john"; username = "john";
hostname = "janus"; hostname = "janus";
ipv4 = "192.168.1.32";
ipv6 = "fded:fb16:653e:25da:be24:11ff:fe6b:4d57";
in in
{ {
flake.modules.nixos.janus-ca =
{ config, pkgs, lib, ... }:
let
cfg = config.janus-ca;
johnHome = lib.attrByPath [ "users" "users" username "home" ] "/home/${username}" config;
johnGroup = lib.attrByPath [ "users" "users" username "group" ] username config;
cfgInEtc = lib.hasPrefix "/etc/" cfg.certDir;
certDirEtcPath =
if cfgInEtc then
lib.removePrefix "/etc/" cfg.certDir
else
cfg.certDir;
mkStepRules = home: user: group: [
"d ${home}/.step 0700 ${user} ${group} -"
"d ${home}/.step/config 0700 ${user} ${group} -"
"d ${home}/.step/certs 0700 ${user} ${group} -"
"L+ ${home}/.step/config/defaults.json - - - - /etc/step-ca/defaults.json"
"L+ ${home}/.step/certs/root_ca.crt - - - - ${cfg.certDir}/root_ca.crt"
];
in
{
options.janus-ca = {
certDir = lib.mkOption {
description = "String path to where the mtls certs will be stored.";
type = lib.types.str;
default = "/etc/step-ca/certs";
};
};
config = {
environment.etc = lib.mkIf cfgInEtc {
"step-ca/defaults.json".text = builtins.toJSON {
ca-url = "https://janus.john-stream.com/";
fingerprint = "2036c44f7b5901566ff7611ea6c927291ecc6d2dd00779c0eead70ec77fa10d6";
root = ./root_ca.crt;
};
};
systemd.tmpfiles.rules =
mkStepRules johnHome username johnGroup
++ mkStepRules "/root" "root" "root";
};
};
flake.nixosConfigurations."${hostname}" = inputs.nixpkgs.lib.nixosSystem { flake.nixosConfigurations."${hostname}" = inputs.nixpkgs.lib.nixosSystem {
modules = with inputs.self.modules; [ modules = with inputs.self.modules; [
nixos.lxc nixos.lxc
nixos.mysops nixos.mysops
nixos.step-ssh-host nixos.ssh-certs
nixos.janus-ca nixos.step-client
nixos.step-ca
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
nixos."${username}" nixos."${username}"
nixos.docker nixos.docker
nixos.login-text nixos.login-text
nixos.mtls nixos.mtls
({ lib, pkgs, ... }: { ({ config, lib, pkgs, ... }: {
networking.hostName = hostname; networking.hostName = hostname;
sops.defaultSopsFile = ../../../keys/secrets.yaml; loginText.extraServiceStatus = {
step-ssh-host = { "Step-CA" = "step-ca";
hostname = hostname;
extraPrincipals = [
"192.168.1.244"
"fded:fb16:653e:25da:be24:11ff:fea0:753f"
];
}; };
sops.defaultSopsFile = ./secrets.yaml;
ssh-certs = {
hostname = hostname;
extraPrincipals = [ ipv4 ipv6 ];
};
step-ca = {
rootCertPath = ./root_ca.crt;
intermediateCertPath = ./intermediate_ca.crt;
dnsNames = [
"${hostname}.john-stream.com"
ipv4
ipv6
];
secrets = {
sopsFile = ./secrets.yaml;
caPassword = "janus/ca_password";
intermediateKey = "janus/intermediate_ca_key";
sshHostCaKey = "janus/ssh_host_ca_key";
sshUserCaKey = "janus/ssh_user_ca_key";
adminProvisionerEncryptedKey = "janus/admin_provisioner_encrypted_key";
};
};
step-client.caUrl = "https://${ipv4}/";
mtls = { mtls = {
enable = true; enable = true;
subject = hostname; subject = hostname;
san = [ san = [
"${hostname}.john-stream.com" "${hostname}.john-stream.com"
"192.168.1.244" ipv4
]; ];
bootstrap = {
enable = true;
after = [ "sops-nix.service" "step-ca.service" ];
wants = [ "step-ca.service" ];
provisionerPasswordFile = config.sops.secrets."janus/admin_jwk".path;
};
}; };
users.users."${username}" = { users.users."${username}" = {
@@ -90,6 +74,7 @@ in
home-manager.users."${username}" = { home-manager.users."${username}" = {
imports = with inputs.self.modules.homeManager; [ imports = with inputs.self.modules.homeManager; [
mysops mysops
step-client
]; ];
docker.enable = true; docker.enable = true;
}; };
+12
View File
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBuzCCAWKgAwIBAgIQIW2tnzaR3eHv3EzooE+QGjAKBggqhkjOPQQDAjAoMQ4w
DAYDVQQKEwVKYW51czEWMBQGA1UEAxMNSmFudXMgUm9vdCBDQTAeFw0yNTEyMTgw
NjAyMjlaFw0zNTEyMTYwNjAyMjlaMDAxDjAMBgNVBAoTBUphbnVzMR4wHAYDVQQD
ExVKYW51cyBJbnRlcm1lZGlhdGUgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
AARyjfmHDMD+aOIESYF9v9Pe68aRgINpjRskGdELz4Y5+x7QNHvbZPEdkox0Qbcp
XETmrcrw6fxBnImYJM5/IU0do2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
BAgwBgEB/wIBADAdBgNVHQ4EFgQUZN0zt5HKZLj0YtGTI7ROw8wLCw8wHwYDVR0j
BBgwFoAUaOeW8ssqNrHjz/vM8/rjOJlzVfswCgYIKoZIzj0EAwIDRwAwRAIgE6W7
yPG7tpLdwAUVF8BtOZV8TEDwmS1gudrcEbgXyXcCIETTUmM4GlN+zxMGYKs6OybN
IxTY4WmviiXMsDoGWm0B
-----END CERTIFICATE-----
+40
View File
@@ -0,0 +1,40 @@
janus:
ca_password: ENC[AES256_GCM,data:z6i0ELrn6XwVbJ7gP1GkpWe0Xw==,iv:3pZs/kzPp6bL8iv8jITZnAHuCycbSjhWswgzuJuLvjE=,tag:3gUftdyVXPVWe5dGxh9dvw==,type:str]
intermediate_ca_key: ENC[AES256_GCM,data:tHP5V/yyrx7ydebg+cN30sLlRhnn01oWogywCUVm4YpKbWTJASaH4G74aaqPAGqETD1Go0Y5xR4DRkBJHBlTc3qzNj5/pzrSKwqfc537SCSUX79hTzw6MvTvukmbg4c/vF0ts5vZPYyi6mYwiAD2Ouk8HwU+AZOlCSm0VWL6rFjmq++fX4jhV0JRpcViBD8tjXpElpT/btyPAAXjaJgqMMZKALfwnAgXXrdy5E5FsuegQRfBadCDLsv0A+qq14CnstfPRkYAKqAhTL2HlmKKVtRiWTMZ6D14dYteUW+NQ33zKVIDz2a0M8tCp+41bDQWC9KrxrKcNVWc6ajAW11DzufP+lVhyWoZF+jkHZjWuy0lsgAUsTvZggLPU6euXHAjDeO4Z3ZQHvViVM9pVnsMzN6/w0i7gHSBFBw=,iv:actN7L8G7xbh9dYq7zQoOvnDYtcqAodV1V5Yu1q5elY=,tag:vas5E1ywf3sP1knVd7iE7A==,type:str]
ssh_host_ca_key: ENC[AES256_GCM,data:eD0aZskYgsN7NWWo98VzrJGfQsbXLPUv9tWz8/GPU9UJcDaDmjEOs96cdW08IvINZXTMJtUscMGmewWX5mg1zevYGBYF8xgfdzT1pao5tw/F079aLc8HNxRn1s+4lL/GINtRXXK13tzYTPeI6Z1g4AE8UqVDZ/65+oLzkzWljE93UM6G9K78+ulBZq27Www0+dqT00n03YSfDCO8LgEJjxo03aShLXth+nT2SGZDfH9sILkZaDOeqOgQpW43L9rU+5yBCcGgCkBuhb8jMu2yStLCRxDewTNcxDYvIsNr1T/7jammRgUn+lYNcKgHeZS81dgNQkXmzRwErYbAx22zCaHdmSlH7RvF0olVCWzgXY4uSjbMFmQqNVVlj7UpYXRIIWnmiiwiBJQlmCJbm/OjS43dTYwZZ0waRIs=,iv:0TTQe2DC6EFiNhFEA3DVv2yu487pr4iX7siooA3EU94=,tag:mlZIquPBWR/5OVU7ZW0Uqg==,type:str]
ssh_user_ca_key: ENC[AES256_GCM,data:CrjtkGfaUklNsOfIWhHwJXzBCJN2RvJKZQLueqAaOmY5niMdVRRnQahtMCtyJ7QxONs26VZkdcLmq4mfHWUA8lo4Q5ssFvqWvIcHvj4J0i0eB4RqQev2KGo/lg+GybZK2opBWS5z7msfPI94Fgu12qN46brBdbgVgclxqS23MR+A8d+VTcUYRcrPLhf4VTPH8eNGnhlM47tjzp7PjpMOumpkjbrNnNyPG3rVzvH0zbCc2HhJxtstEdoLT1u+1FxphR6kroRd/F+xQAY82A4UYVuHfaNkc8thHLA+wbGf5ReRZode5vCz4ErEWpJhnltU8YVgMDQSqE5gDuxWFv+P17hUdNXUmVI8QBiyu0sI8oNIBT6xDvK69rgzKaEy8MON/2E31CpQALokEiDB1Xn04W3rdnSVS6m84ck=,iv:lGrSv/0qCXha7rK/nEtZa0cH2wKtGxKigUsxqahk3SE=,tag:ZmxenXfXt9Pa4euZIgt1bw==,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:6NxQm3pHzHmBkGJq/YLswi7uyMXzbzrsBPoqhpFA25PmHJBeIUVi0sIWPsdGDemx8dzEZ3oEB4dGbZ8JSvywpnSdvIzUhWK21BZLpbutUDBFtG68tkFK5N7dJ/dlA000oNEq5JiqrlKdFuVVjtUj7BK887a5xK7gyYfiqvv4E5dWv9IGk7lCHb/tjKkXGpYTRg1MVv2wSoVGRRSW1/97s2gAFMzeG48yJlwfiCQERXpHBte2rW5vZrOyR57J0ienf75wYwz6dLJuM1YZLKP9Ob8hCs00PEyxq7+JZuX/tx2yRiYKlgCYhtVbLbnjriPnSpKB/Xn/5t2FGP3ogfWUx+xkUm8lHDPbzrI86LNpFo016Ge2gDCDLvqLr9o4SDqnwmJfiAdMQokH7rJuQOiqk8gnWQCk8cvKc3vdYE0F6TnjbnZHMU4JbnmCr40xkBXkya/9jxVAC8RYfaW88hmAfQ9mYSdnbbUipC9Hv9Qhwst+Yqe/HxwaWfyimE0CpxOopwUT2qT/+ZO8Qjy/HnK4,iv:jlWTcKt/fvrry/OTamtKHNZAZmlxAj6TgGoPDor4HeY=,tag:btP8xFMEDzd/Nzr4EqIDZg==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRZ1pQQW1rekYzUDFGcXBW
M2RHSXUvbCtvemtHYUljNmxXOCt4dEczYUU0CkdFWDRBYWRhQStYb1RtbEFmZVk4
bWxMTnhKVXlZWDRlcWZHWWhaSC9KMW8KLS0tIER6VWJGY28ramk2NDhZa0x5RU1s
YmU5S1FCVkJyNTNIVXhJSkdQVHYxMVkKEkbFmD4a7FWC8cKpZu2ZVot5ibheTQ1h
H6uqwAYT/UIXqnceyZyoT9JRcZyQy7xAUAJzQMe+TxUr+8iHAm7aBg==
-----END AGE ENCRYPTED FILE-----
recipient: age1qahhlzeanprtykym9jymk2t95uedr7cwx9sdshx46q2m6u66fucsqua8l3
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqWjBiVDJSZFRQYis3dSt5
OFFMbGhyaHgxSDBjdnVMSlZ5d3B0eEhVM0VVCmw4TXk2a0s3ZDRCeHhEVFpGRkpl
b1lSQUJPL0hNRk50UEhHejNkL2VRd1EKLS0tICtXaWVMN0FSejNsNGNTOGh1WG13
aFJINlNKOVNhc3VBOU00VXYyMTRFdVkKV251o5CegRdDxvhpOJjK5ITp/+DVBE9o
R3eAsTXG5SSN50rcZYjwoEgYUeiRB4dQ/NYw6lVDUFrOf/a7sAr32g==
-----END AGE ENCRYPTED FILE-----
recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYYjlQRGVmRnRGbjNNT3dC
cWJCWmtrM1BnbE80QnpTWXdHMytqSmFHOVhjCm9LYzd4R2s1TENIVkFYK3g4STRk
cGIxMS9RQ2Y1dmR6RjZYeVF5LzRoelkKLS0tIEdjaGk2WnNCRVA0S1dxRzJyWFc3
Yzh0MlFSZTR0Z0RhUnhHQzRSbFRXaUEK+5jadT4PMWPrljNcXVW3S1yJ1nI0iei8
htf09S2S5jraIFkl2x6Rv8IqOnKVPPgTZkvZqPMQoM4zcs+o1/lnug==
-----END AGE ENCRYPTED FILE-----
recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
lastmodified: "2026-07-05T01:59:20Z"
mac: ENC[AES256_GCM,data:s4HAH/T9iqHooXXswMoeC1ttJwpyXQgnzy4Op2WPP55nowM//itnxpBzaqlMQUuuqMi4kd8wD/AakrlPRsbsBjgxnPYqmBKQA5eg+3a8QSf31HcLEbhtOsi38jOG2L2VMkdVfihvbe8NdFw4AD1tPviDhs7t9zjWEH1r/eCu+as=,iv:+FmOEuFM0V1ID3zkvNXF8m0ukQpq0Nmbdp0s4/WR/PY=,tag:/e4xkd2bdfC5fdHXJB4NAA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1
+1
View File
@@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ=
@@ -56,6 +56,7 @@
gnome gnome
desktop desktop
mysops mysops
step-client
rebuild rebuild
{ {
my-vscode.enable = true; my-vscode.enable = true;
+1 -1
View File
@@ -12,7 +12,6 @@
"${inputs.nixos-hardware}/lenovo/thinkpad/p14s/amd/gen4" "${inputs.nixos-hardware}/lenovo/thinkpad/p14s/amd/gen4"
] ++ (with self.modules.nixos; [ ] ++ (with self.modules.nixos; [
p14sConfiguration p14sConfiguration
janus-ca
rebuild rebuild
sudo sudo
john john
@@ -20,6 +19,7 @@
steam steam
wireguard wireguard
mtls mtls
step-client
# greetd # greetd
# niri # niri
]); ]);
+1 -1
View File
@@ -59,7 +59,7 @@ in
ssh = { ssh = {
certificates.enable = true; certificates.enable = true;
knownHosts = [ knownHosts = [
"@cert-authority fded:fb16:653e:25da:be24:11ff:fea0:753f ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ=" "@cert-authority fded:fb16:653e:25da:be24:11ff:* ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ="
]; ];
matchSets = { matchSets = {
certs = true; certs = true;
+5 -6
View File
@@ -8,19 +8,18 @@ in
flake.nixosConfigurations."${hostname}" = inputs.nixpkgs.lib.nixosSystem { flake.nixosConfigurations."${hostname}" = inputs.nixpkgs.lib.nixosSystem {
modules = with inputs.self.modules; [ modules = with inputs.self.modules; [
nixos.lxc nixos.lxc
nixos."${username}"
nixos.mysops nixos.mysops
nixos.step-ssh-host nixos."${username}"
nixos.ssh-certs
nixos.login-text nixos.login-text
nixos.docker nixos.docker
nixos.mtls nixos.mtls
nixos.janus-ca nixos.step-client
nixos.forgejo nixos.forgejo
nixos.restic-server nixos.restic-server
# nixos.restic-envoy # nixos.restic-envoy
({ config, pkgs, ... }: { ({ config, pkgs, ... }: {
networking.hostName = hostname; networking.hostName = hostname;
time.timeZone = "America/Chicago";
# Removes password for sudo # Removes password for sudo
security.sudo-rs.extraRules = lib.mkAfter [ security.sudo-rs.extraRules = lib.mkAfter [
@@ -38,7 +37,6 @@ in
users.users."${username}".extraGroups = [ "mtls" ]; users.users."${username}".extraGroups = [ "mtls" ];
mtls = { mtls = {
enable = true; enable = true;
certDir = config.janus-ca.certDir;
subject = hostname; subject = hostname;
san = [ san = [
"${hostname}.john-stream.com" "${hostname}.john-stream.com"
@@ -77,7 +75,7 @@ in
"Restic REST Server" = "restic-rest-server.service"; "Restic REST Server" = "restic-rest-server.service";
}; };
step-ssh-host.hostname = hostname; ssh-certs.hostname = hostname;
# This provides the secrets at install time # This provides the secrets at install time
sops.defaultSopsFile = ./secrets.yaml; sops.defaultSopsFile = ./secrets.yaml;
@@ -98,6 +96,7 @@ in
imports = [ imports = [
inputs.self.modules.homeManager.rebuild inputs.self.modules.homeManager.rebuild
inputs.self.modules.homeManager.mysops inputs.self.modules.homeManager.mysops
inputs.self.modules.homeManager.step-client
({ config, pkgs, lib, ... }: { ({ config, pkgs, lib, ... }: {
homeManagerFlakeDir = "${config.xdg.configHome}/home-manager"; homeManagerFlakeDir = "${config.xdg.configHome}/home-manager";
docker.enable = true; docker.enable = true;
-2
View File
@@ -1,5 +1,3 @@
janus:
admin_jwk: ENC[AES256_GCM,data:2XcN5X77wTQ+OUXa4C9xAErGvrwJKseHjCcgoj6jt+c=,iv:9x+M1wM0dYND1JYkJjM4N8pSOok2OF+P9vmm9NCumTI=,tag:dTCfh+KB1iRJBVoNsGqvuw==,type:str]
forgejo: forgejo:
#ENC[AES256_GCM,data:/wtm0uXbiWFoGNWtlTzVuNxBR7CPm2FMB98t3AxSj5V5rltLvzF9BjgWoJCiX3ltzmU=,iv:xaZXbUIGJHxPrLRQzEQI7hgRgc0y061jIhoE3zlcMaA=,tag:fQGheCIdBKm6wE+vtj7g6A==,type:comment] #ENC[AES256_GCM,data:/wtm0uXbiWFoGNWtlTzVuNxBR7CPm2FMB98t3AxSj5V5rltLvzF9BjgWoJCiX3ltzmU=,iv:xaZXbUIGJHxPrLRQzEQI7hgRgc0y061jIhoE3zlcMaA=,tag:fQGheCIdBKm6wE+vtj7g6A==,type:comment]
secret_key: ENC[AES256_GCM,data:/jcyeDcsryLqu9Q3VnNaENb71/Tl5JUr0zDzxt8L5UCtnJ/YHA6MKItrQ9ZHFv1XROtnSfZl9D5kKomeM8EVqA==,iv:HxMKAMVQ08gkq6SWENj0/d8i9PhcgPCp5eqbztj9bSg=,tag:nO2dFWT/vfgGc/9JIDZrGw==,type:str] secret_key: ENC[AES256_GCM,data:/jcyeDcsryLqu9Q3VnNaENb71/Tl5JUr0zDzxt8L5UCtnJ/YHA6MKItrQ9ZHFv1XROtnSfZl9D5kKomeM8EVqA==,iv:HxMKAMVQ08gkq6SWENj0/d8i9PhcgPCp5eqbztj9bSg=,tag:nO2dFWT/vfgGc/9JIDZrGw==,type:str]
+1 -1
View File
@@ -3,7 +3,7 @@
let let
defaultServiceStatus = { defaultServiceStatus = {
SSH = "sshd.socket"; SSH = "sshd.socket";
"SSH Cert Renewal" = "step-ssh-host-renew.timer"; "SSH Cert Renewal" = "ssh-certs-renew.timer";
}; };
in { in {
options.loginText.extraServiceStatus = lib.mkOption { options.loginText.extraServiceStatus = lib.mkOption {
+39 -9
View File
@@ -1,13 +1,14 @@
{ inputs, ... }: { inputs, ... }:
let let
userName = "john"; userName = "john";
sshHostCAPubKey = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ="; sshHostCAPubKeyPath = ../hosts/janus/ssh_host_ca_key.pub;
in in
{ {
flake.modules.nixos.ssh = { config, pkgs, lib, ... }: flake.modules.nixos.ssh = { config, pkgs, lib, ... }:
let let
cfg = config.ssh; cfg = config.ssh;
configDir = "/etc/ssh"; configDir = "/etc/ssh";
sshHostCAPubKey = lib.removeSuffix "\n" (builtins.readFile sshHostCAPubKeyPath);
in in
{ {
options.ssh = { options.ssh = {
@@ -16,17 +17,43 @@ in
type = lib.types.str; type = lib.types.str;
default = "ssh_host_ed25519_key"; default = "ssh_host_ed25519_key";
}; };
hostKeyType = lib.mkOption {
description = "OpenSSH host key type for ssh.hostKey.";
type = lib.types.enum [ "ed25519" "rsa" "ecdsa" ];
default = "ed25519";
};
fallbackHostKeys = lib.mkOption {
description = "Additional non-certificate host keys that keep sshd reachable during certificate bootstrap.";
type = with lib.types; listOf (submodule {
options = {
path = lib.mkOption {
description = "Path to the host private key file.";
type = str;
};
type = lib.mkOption {
description = "OpenSSH host key type.";
type = enum [ "ed25519" "rsa" "ecdsa" ];
};
};
});
default = [
{
path = "${configDir}/ssh_host_rsa_key";
type = "rsa";
}
];
};
certificates = { certificates = {
enable = lib.mkEnableOption "Enable SSH host certificates"; enable = lib.mkEnableOption "Enable SSH host certificates";
userCA = lib.mkOption { userCA = lib.mkOption {
description = "Content for the SSH user CA file (public key)"; description = "Content for the SSH user CA file (public key)";
type = lib.types.path; type = lib.types.path;
default = ../hosts/janus/ssh_user_ca.pub; default = sshHostCAPubKeyPath;
}; };
userCAFile = lib.mkOption { userCAFile = lib.mkOption {
description = "String path to the SSh user CA"; description = "String path to the SSh user CA";
type = lib.types.str; type = lib.types.str;
default = "ssh_user_ca.pub"; default = "ssh_user_ca_key.pub";
}; };
}; };
}; };
@@ -34,12 +61,17 @@ in
config = { config = {
services.openssh = { services.openssh = {
enable = true; enable = true;
hostKeys = cfg.fallbackHostKeys ++ [
{
path = "${configDir}/${cfg.hostKey}";
type = cfg.hostKeyType;
}
];
# require public key authentication for better security # require public key authentication for better security
settings = lib.mkMerge [ settings = lib.mkMerge [
{ {
PasswordAuthentication = false; PasswordAuthentication = false;
KbdInteractiveAuthentication = false; KbdInteractiveAuthentication = false;
HostKey = "${configDir}/${cfg.hostKey}";
} }
(lib.mkIf cfg.certificates.enable { (lib.mkIf cfg.certificates.enable {
TrustedUserCAKeys = "${configDir}/${cfg.certificates.userCAFile}"; TrustedUserCAKeys = "${configDir}/${cfg.certificates.userCAFile}";
@@ -72,6 +104,7 @@ in
identityFile = cfg.identityFile; identityFile = cfg.identityFile;
publicKeyFile = "${identityFile}.pub"; publicKeyFile = "${identityFile}.pub";
certificateFile = "${identityFile}-cert.pub"; certificateFile = "${identityFile}-cert.pub";
sshHostCAPubKey = lib.removeSuffix "\n" (builtins.readFile sshHostCAPubKeyPath);
in in
{ {
options.ssh = with lib; { options.ssh = with lib; {
@@ -106,10 +139,7 @@ in
}; };
}; };
# All this stuff has to be wrapped in a config attribute because of the presence of the options here? config = {
config = let
provisionerPasswordPath = config.sops.secrets."janus/admin_jwk".path;
in {
home.file.".ssh/known_hosts" = { home.file.".ssh/known_hosts" = {
text = lib.concatStringsSep "\n" ( text = lib.concatStringsSep "\n" (
cfg.knownHosts ++ lib.optionals cfg.certificates.enable [ cfg.knownHosts ++ lib.optionals cfg.certificates.enable [
@@ -165,7 +195,7 @@ in
}) })
(lib.mkIf cfg.matchSets.certs { (lib.mkIf cfg.matchSets.certs {
"janus" = { "janus" = {
HostName = "janus.john-stream.com"; HostName = "fded:fb16:653e:25da:be24:11ff:fe6b:4d57";
User = "root"; User = "root";
}; };
"soteria" = { "soteria" = {
-129
View File
@@ -1,129 +0,0 @@
{
"root": "/etc/step-ca/certs/root_ca.crt",
"federatedRoots": null,
"crt": "/etc/step-ca/certs/intermediate_ca.crt",
"key": "/etc/step-ca/secrets/intermediate_ca_key",
"address": ":443",
"insecureAddress": "",
"dnsNames": [
"janus.john-stream.com",
"192.168.1.113"
],
"ssh": {
"hostKey": "/etc/step-ca/secrets/ssh_host_ca_key",
"userKey": "/etc/step-ca/secrets/ssh_user_ca_key"
},
"logger": {
"format": "text"
},
"db": {
"type": "badgerv2",
"dataSource": "/var/lib/step-ca/db",
"badgerFileLoadingMode": ""
},
"authority": {
"provisioners": [
{
"type": "ACME",
"name": "acme"
},
{
"type": "SSHPOP",
"name": "sshpop",
"claims": {
"enableSSHCA": true
}
},
{
"type": "JWK",
"name": "admin",
"key": {
"use": "sig",
"kty": "EC",
"kid": "xoxgOJFbveSLIL2gm1Yu5ZiRb9v8Jxe44F56i3v-Nf8",
"crv": "P-256",
"alg": "ES256",
"x": "zFO8hPx_eH0Iyz7UJI-w8ODMusEKCZ28M76sGWmWYxA",
"y": "XIWLLyKDzqxV9UH-2KeAkKPDrgLoPrxxW9-PzkXggME"
},
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjYwMDAwMCwicDJzIjoiUVJnTnJVTF9KcmxJYkJMVTlGNVRPZyJ9.DMu7xBNCq5pr-_--YTxNr5Hrcqy6ZmSVHsWurfVXL7Hk0Q3vyYRxiw.h-CnFiYc-DhxThI3.plx3_Qa_0kU-2TwnqFNfAfGnCpfQ2e0iiCMLruNHbLMnHeXQ1BysHBqps45_02zZXIRdHoDgYGtXRSfcdUYYoS0pLoPzC6m301ZFNSAFdRVlSZ3Q6VmWdixPXXnEB4EgSKTT_wxR33L8t9OpFzD85KfY-b_Un1l99ufjCnfg-EYkcICTn_G4-8bcW3eFIvJ6setzu-l0jHMhLQdIweqncn9on9xBXBD-ANhZfP95P2BJt-APqCi8eqiAvn_vClovdg0PxzRwOVDvWREz66FDw-HTU7xDtGO9hACopT5tfZOXDoykgZw1mJsq9NEq9ZzvKG2hvyk1UXtExxrNtFo.5q1OfGU4Amo4Si-vpeI42g",
"claims": {
"enableSSHCA": true,
"disableRenewal": false,
"allowRenewalAfterExpiry": false,
"disableSmallstepExtensions": false
},
"options": {
"x509": {},
"ssh": {}
}
}
],
"template": {},
"backdate": "1m0s"
},
"tls": {
"cipherSuites": [
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
],
"minVersion": 1.2,
"maxVersion": 1.3,
"renegotiation": false
},
"templates": {
"ssh": {
"user": [
{
"name": "config.tpl",
"type": "snippet",
"template": "templates/ssh/config.tpl",
"path": "~/.ssh/config",
"comment": "#"
},
{
"name": "step_includes.tpl",
"type": "prepend-line",
"template": "templates/ssh/step_includes.tpl",
"path": "${STEPPATH}/ssh/includes",
"comment": "#"
},
{
"name": "step_config.tpl",
"type": "file",
"template": "templates/ssh/step_config.tpl",
"path": "ssh/config",
"comment": "#"
},
{
"name": "known_hosts.tpl",
"type": "file",
"template": "templates/ssh/known_hosts.tpl",
"path": "ssh/known_hosts",
"comment": "#"
}
],
"host": [
{
"name": "sshd_config.tpl",
"type": "snippet",
"template": "templates/ssh/sshd_config.tpl",
"path": "/etc/ssh/sshd_config",
"comment": "#",
"requires": [
"Certificate",
"Key"
]
},
{
"name": "ca.tpl",
"type": "snippet",
"template": "templates/ssh/ca.tpl",
"path": "/etc/ssh/ca.pub",
"comment": "#"
}
]
}
},
"commonName": "Step Online CA"
}
+52 -24
View File
@@ -1,7 +1,7 @@
{ inputs, ... }: { { inputs, ... }: {
flake.modules.nixos.step-ssh-host = { config, pkgs, lib, ... }: flake.modules.nixos.ssh-certs = { config, pkgs, lib, ... }:
let let
cfg = config.step-ssh-host; cfg = config.ssh-certs;
provisionerPasswordPath = config.sops.secrets."janus/admin_jwk".path; provisionerPasswordPath = config.sops.secrets."janus/admin_jwk".path;
sshKeyPath = "/etc/ssh/ssh_host_ed25519_key"; sshKeyPath = "/etc/ssh/ssh_host_ed25519_key";
sshCertPath = "${sshKeyPath}-cert.pub"; sshCertPath = "${sshKeyPath}-cert.pub";
@@ -11,10 +11,28 @@
cfg.hostname cfg.hostname
"${cfg.hostname}.john-stream.com" "${cfg.hostname}.john-stream.com"
] ++ cfg.extraPrincipals); ] ++ 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 in
{ {
# NixOS Options # NixOS Options
options.step-ssh-host = { options.ssh-certs = {
hostname = lib.mkOption { hostname = lib.mkOption {
description = "Networking host name to register with the CA"; description = "Networking host name to register with the CA";
type = lib.types.str; type = lib.types.str;
@@ -37,28 +55,23 @@
config = { config = {
ssh.certificates.enable = true; ssh.certificates.enable = true;
sops.secrets."janus/admin_jwk" = { sops.secrets."janus/admin_jwk" = {
# Shared provisioner credential is intentionally centralized.
sopsFile = ../../../keys/secrets.yaml;
owner = "root"; owner = "root";
group = "root"; group = "root";
mode = "0400"; mode = "0400";
}; };
networking.nameservers = [ "192.168.1.150" ]; networking.nameservers = [ "192.168.1.150" ];
networking.dhcpcd.extraConfig = "nohook resolv.conf"; networking.dhcpcd.extraConfig = "nohook resolv.conf";
environment.systemPackages = with pkgs; [ environment.systemPackages = [
# step-cli sshHostCertRenew
(writeShellScriptBin "ssh-host-cert-renew" '' sshHostCertCheck
${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}")
]; ];
systemd.services.step-ssh-host-renew = { systemd.services.ssh-certs-renew = {
description = "Renew Step SSH host certificate if needed"; description = "Renew Step SSH host certificate if needed";
wantedBy = [ ]; wantedBy = [ "multi-user.target" ];
before = [ "sshd.service" ];
after = [ "network-online.target" ]; after = [ "network-online.target" ];
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
path = with pkgs; [ coreutils systemd step-cli openssh ]; path = with pkgs; [ coreutils systemd step-cli openssh ];
@@ -69,8 +82,14 @@
}; };
script = '' script = ''
set -euo pipefail 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 else
rc=$? rc=$?
if [ "$rc" -eq 1 ]; then if [ "$rc" -eq 1 ]; then
@@ -79,17 +98,22 @@
fi fi
if [ "$rc" -eq 2 ]; then if [ "$rc" -eq 2 ]; then
echo "SSH host cert missing: ${sshCertPath}" >&2 echo "SSH host cert missing or unreadable: ${sshCertPath}"
exit 1 renew=1
fi else
echo "step ssh needs-renewal failed with rc=$rc" >&2 echo "step ssh needs-renewal failed with rc=$rc" >&2
exit "$rc" exit "$rc"
fi fi
fi
if [ "$renew" -eq 1 ]; then
${lib.getExe sshHostCertRenew}
${lib.getExe sshHostCertCheck}
fi
''; '';
}; };
systemd.timers.step-ssh-host-renew = { systemd.timers.ssh-certs-renew = {
description = "Periodic Step SSH host certificate renewal"; description = "Periodic Step SSH host certificate renewal";
wantedBy = [ "timers.target" ]; wantedBy = [ "timers.target" ];
@@ -98,9 +122,13 @@
OnUnitActiveSec = "4h"; OnUnitActiveSec = "4h";
RandomizedDelaySec = "15m"; RandomizedDelaySec = "15m";
Persistent = true; Persistent = true;
Unit = "step-ssh-host-renew.service"; Unit = "ssh-certs-renew.service";
}; };
}; };
# Ensure sshd waits for a cert reconciliation attempt at boot.
systemd.services.sshd.wants = [ "ssh-certs-renew.service" ];
systemd.services.sshd.after = [ "ssh-certs-renew.service" ];
}; };
}; };
} }
+177 -12
View File
@@ -1,25 +1,190 @@
{ inputs, ... }: { inputs, ... }:
let
ipAddress = "0.0.0.0";
in
{ {
flake.modules.nixos.step-ca = { pkgs, ... }: { flake.modules.nixos.step-ca = { config, pkgs, lib, ... }:
let
cfg = config.step-ca;
caAddress = "0.0.0.0";
caPort = 443;
caPasswordPath = (lib.getAttr cfg.secrets.caPassword config.sops.secrets).path;
intermediateKeyPath = (lib.getAttr cfg.secrets.intermediateKey config.sops.secrets).path;
sshHostCaKeyPath = (lib.getAttr cfg.secrets.sshHostCaKey config.sops.secrets).path;
sshUserCaKeyPath = (lib.getAttr cfg.secrets.sshUserCaKey config.sops.secrets).path;
adminProvisionerEncryptedKeyValue =
(lib.getAttr cfg.secrets.adminProvisionerEncryptedKey config.sops.placeholder);
renderedStepCaConfig = builtins.toJSON {
root = cfg.rootCertPath;
crt = cfg.intermediateCertPath;
key = intermediateKeyPath;
address = "${caAddress}:${toString caPort}";
dnsNames = cfg.dnsNames;
ssh = {
hostKey = sshHostCaKeyPath;
userKey = sshUserCaKeyPath;
};
db = {
type = "badgerv2";
dataSource = "/var/lib/step-ca/db";
};
authority = {
backdate = "1m0s";
provisioners = [
{
type = "ACME";
name = "acme";
}
{
type = "SSHPOP";
name = "sshpop";
claims.enableSSHCA = true;
}
{
type = "JWK";
name = "admin";
key = {
use = "sig";
kty = "EC";
kid = "xoxgOJFbveSLIL2gm1Yu5ZiRb9v8Jxe44F56i3v-Nf8";
crv = "P-256";
alg = "ES256";
x = "zFO8hPx_eH0Iyz7UJI-w8ODMusEKCZ28M76sGWmWYxA";
y = "XIWLLyKDzqxV9UH-2KeAkKPDrgLoPrxxW9-PzkXggME";
};
encryptedKey = adminProvisionerEncryptedKeyValue;
claims = {
enableSSHCA = true;
disableRenewal = false;
allowRenewalAfterExpiry = false;
disableSmallstepExtensions = false;
};
options = {
x509 = { };
ssh = { };
};
}
];
};
tls = {
cipherSuites = [
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
];
minVersion = 1.2;
maxVersion = 1.3;
renegotiation = false;
};
};
in
{
options.step-ca = {
rootCertPath = lib.mkOption {
description = "Path to the Step CA root certificate served by this host.";
type = lib.types.path;
};
intermediateCertPath = lib.mkOption {
description = "Path to the Step CA intermediate certificate served by this host. This is public material and does not need to be stored in SOPS.";
type = lib.types.path;
};
dnsNames = lib.mkOption {
description = "DNS names and IP SANs advertised by this Step CA instance.";
type = with lib.types; listOf str;
};
secrets = {
sopsFile = lib.mkOption {
description = "Host-local SOPS file that stores Step CA secret material.";
type = lib.types.path;
};
caPassword = lib.mkOption {
description = "SOPS key for the Step CA intermediate password.";
type = lib.types.str;
};
intermediateKey = lib.mkOption {
description = "SOPS key for the Step CA intermediate private key.";
type = lib.types.str;
};
sshHostCaKey = lib.mkOption {
description = "SOPS key for the Step SSH host CA private key.";
type = lib.types.str;
};
sshUserCaKey = lib.mkOption {
description = "SOPS key for the Step SSH user CA private key.";
type = lib.types.str;
};
adminProvisionerEncryptedKey = lib.mkOption {
description = "SOPS key for the Step CA admin provisioner encrypted key.";
type = lib.types.str;
};
};
};
config = {
# Placeholders are expected initially until real material is inserted into sops.
sops.secrets."${cfg.secrets.caPassword}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.intermediateKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.sshHostCaKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.sshUserCaKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.adminProvisionerEncryptedKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.templates."step-ca-config" = {
owner = "step-ca";
group = "step-ca";
mode = "0400";
content = renderedStepCaConfig;
};
# https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/security/step-ca.nix # https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/security/step-ca.nix
services.step-ca = { services.step-ca = {
enable = true; enable = true;
openFirewall = true; openFirewall = true;
address = ipAddress; address = caAddress;
port = 8443; port = caPort;
intermediatePasswordFile = caPasswordPath;
};
environment.etc."smallstep/ca.json".source =
lib.mkForce config.sops.templates."step-ca-config".path;
systemd.services.step-ca.restartTriggers =
lib.mkAfter [ config.sops.templates."step-ca-config".path ];
# https://smallstep.com/docs/step-ca/configuration/#configuration-options
settings = {
root = "";
crt = "";
};
};
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
step-ca step-ca
step-cli step-cli
]; ];
}; };
};
} }
+14 -1
View File
@@ -31,7 +31,10 @@ in
# This module will be imported by the user factory # This module will be imported by the user factory
homeManager."${username}" = { pkgs, ... }: homeManager."${username}" = { pkgs, ... }:
with inputs.self.meta.users."${username}"; { with inputs.self.meta.users."${username}";
let
selfPkgs = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
in {
home.stateVersion = "25.11"; home.stateVersion = "25.11";
imports = [ imports = [
inputs.self.modules.homeManager.shell-tools inputs.self.modules.homeManager.shell-tools
@@ -39,8 +42,18 @@ in
inputs.self.modules.homeManager.git inputs.self.modules.homeManager.git
]; ];
xdg.enable = true; xdg.enable = true;
home.packages = [
selfPkgs.neovim-min
];
home.sessionVariables = {
EDITOR = "nvim";
VISUAL = "nvim";
GIT_EDITOR = "nvim";
SOPS_EDITOR = "nvim";
};
programs.git.settings.user.name = name; programs.git.settings.user.name = name;
programs.git.settings.user.email = email; programs.git.settings.user.email = email;
programs.git.settings.core.editor = "nvim";
}; };
}; };
} }