41 Commits
Author SHA1 Message Date
John Lancaster b21bfd6bb4 ssh user certs in home-manager 2026-07-09 09:01:34 -05:00
John Lancaster a0f5783bbc jsl-zsh as default shell 2026-07-09 08:07:25 -05:00
John Lancaster addf175fef WIP 2026-07-07 18:56:29 -05:00
John Lancaster b60b91926a reorg ssh wrappers 2026-07-07 18:25:09 -05:00
John Lancaster 4f9e67208b started optionModules.mtls 2026-07-05 23:58:16 -05:00
John Lancaster 118e1d8f4f silencing warnings 2026-07-05 23:57:12 -05:00
John Lancaster 1b98c83d3f breaking up mtls with better wrappers 2026-07-05 22:59:05 -05:00
John Lancaster 39504cd856 reorg 2026-07-05 20:40:28 -05:00
John Lancaster ca75da7c37 soteria off the ground 2026-07-05 20:29:04 -05:00
John Lancaster 9f1fed071d updates for soteria host 2026-07-05 20:05:02 -05:00
John Lancaster bfbdbae99b name tweak 2026-07-05 19:35:10 -05:00
John Lancaster 9f29907ec3 janus module cleanup 2026-07-05 19:34:51 -05:00
John Lancaster 87821ed109 pruned old ssh module 2026-07-05 19:23:32 -05:00
John Lancaster b37ee5777d some ssh match blocks 2026-07-05 19:11:53 -05:00
John Lancaster 68f40feffb moved 1password agent config 2026-07-05 19:04:48 -05:00
John Lancaster a60d2bcd28 moving to john-pc 2026-07-05 19:01:38 -05:00
John Lancaster dc89592eb1 started new ssh module with janus 2026-07-05 18:21:52 -05:00
John Lancaster fa29ce93f4 reorg 2026-07-05 11:26:36 -05:00
John Lancaster 73f5df1832 WIP SSH cert wrappers 2026-07-05 11:16:20 -05:00
John Lancaster 225020eb8d check wrappers 2026-07-05 08:56:32 -05:00
John Lancaster c762c24d86 added github copilot cli 2026-07-05 00:04:42 -05:00
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
John Lancaster cf418ca7a0 root version of jsl-zsh 2026-07-03 21:46:31 -05:00
John Lancaster 8ae74a9bc9 silencing warning 2026-07-03 21:22:50 -05:00
John Lancaster b74133985c pruning zsh 2026-07-03 21:16:00 -05:00
John Lancaster 2de7650f3d jsl-zsh in ghostty 2026-07-03 20:52:31 -05:00
John Lancaster 41f33653a7 central time zone on LXCs 2026-07-02 20:20:12 -05:00
John Lancaster 460d8908bb jsl-zsh working as a login shell 2026-07-02 20:11:49 -05:00
John Lancaster 621a61fb9e trusting ubuntu key by default 2026-07-02 08:38:42 -05:00
John Lancaster 3f3d847134 extra principals for SSH host certs 2026-07-02 08:38:06 -05:00
46 changed files with 1918 additions and 1391 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
+17 -2
View File
@@ -2,13 +2,28 @@ keys:
- &john-p14s age1f6drjusg866yscj8029tk4yfpgecklrvezldm02ankm6h8nnwu5s2u6ahy - &john-p14s age1f6drjusg866yscj8029tk4yfpgecklrvezldm02ankm6h8nnwu5s2u6ahy
- &john-pc age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt - &john-pc age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
- &test-nix age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0 - &test-nix age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
- &soteria age1h0prahyukq4l564yqwgcpg3g6gdrjflk0suklussjjrjstxd9uesws8633 - &soteria age1p4gyh5260ewp7t2ctzv5ewj4a06szl389fm4gzy6vltxhjj73ers87u33g
- &janus age1qahhlzeanprtykym9jymk2t95uedr7cwx9sdshx46q2m6u66fucsqua8l3
creation_rules: creation_rules:
- path_regex: 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:
- *john-pc
- *soteria - *soteria
- *john-pc
- *test-nix - *test-nix
- path_regex: john-p14s/secrets\.yaml$ - path_regex: john-p14s/secrets\.yaml$
key_groups: key_groups:
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+IFgyNTUxOSB5dTVZQ2hFanhqbVJXTmxG
S0NtSi84MjhnN0RORkh2NjZ4YlYvWS9kZDBNClFzYnVxWnhmQkpCRkRFVUx1RDdX YUFFanBiTzgyek1WZW5YajlaL3hVNElORHpJCm1JN2hxell5dmJVczlBbVI2VktH
ZHFqYXRqYXM0cWJzcU5EeEtSR1BUVzAKLS0tIDdEY2pnVTJqWlNZVkZldXVYVmFH Z21lelF6UDBHcXIyUDE0clpxcEV4MGsKLS0tIG0zMktBVEt1SFZ6aGFaNW0wdzlB
dVNBRUVodU5sRnpVcG1GZ1RiZzhjTXMKefqBvvD/qZwcSHmFjUnleukVRLueG36Y QkpQc3lkNmZCd0d6ZVRCc1ZaWjFYaDAK9RXWeW6dCIhYfVGPywsHlRigORVMuVfl
Q81KlwQweF2F8kHl7Bqsi+3hH1dZZbVm3vjuGpWFOoti7fowUV55Kw== d+Evyp72wV5C1MnhV9jv8O4S8aQkcNae1PALacnFw0GJAI1iXn0bqw==
-----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+IFgyNTUxOSBBTWt3MEFDL3JGOUw0LzNa
SFdBYkpxSnAxTUZXbjVwQnlZQ3l1SWtuZGg0CmVBdnVHbTNUcmwvK01iMnZKZTJh RmNLMmFRRzh5NEtZWENjNTlrcDg1MHpjWWg0Cjl5THh3ckdyMWVqR0M2Q0Y0czUr
ajFla3kzYUl4ZWY3czA0WUdNM2lpVFUKLS0tIHo5Uk1pV296MXdnUTZGQ25haWZG Y0ZNRGVIbS9TNXlCcFo3R3lPZUl4cW8KLS0tIDN4QmhSYjBPMDZLWURhMndsK2hE
QWZDWGRaRDBhY1ZkZk5oTHY0ZVV2RXMKanv+WWRhf5nl+aw/T6QZFVQQmhV1DZfB WVY1T0JnZ3dmSjNIMDNBYmhMellvMWcKp15FwF7lJnHoZ/tLNQTGvwP7Kffj8hms
jkSzOAKOgPx7toYFmpq9E8fAH+zrMzDbxI2z2uyrOFI6v+QE0Ul/iQ== 6G5ZG2howsNFiRFFdo/uL08ShaycL2PAL/Kge/0pPL1ygiDaDLjGGg==
-----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+IFgyNTUxOSBwT1FoTDNmRXM4Tk9HZHNu
NittUXN0Qnpld1YvOU50OUh3Z3ZiSzhHOHdNCnc4TmdYbS9QQnBLbldHSytIdkJl ZSszMEM1R0Z5bzY1ZzRvR3VGR2srVVdXbDBNCmp2c0tacndoQ3RiZUpNTnk1MTNH
R0psQWxkZTgyZTRzckkrTGpyNCsvR2sKLS0tIEdLb05aT2I2S3BKcFRrVmtvTGw5 V0Q4ckhWaDBjemYxZEhrSGJHcVlwYzQKLS0tIHNDZkNrNmZvM2plNEp2TTJmVlpT
Z1orRCtkTDVXSktuck5pTmV4K05qZHMKZlHHu07q+GnyDDgdwW2Ic3P23PmoSPwn U1JhN2MrT2NzeURuVzRIZlNEVEd0S0EKBcTG4hWKdXqNfajVZ5DOClOeZsFYktPb
WuNLZdlZQleROaRb+zpD+9P1HGGJ3mWAlNlnmjGrRk453k1PbBQ5Og== dAauLGwx1zkXbVxopUpH5+vCXp02kvvwgO9kiqJvSf0U+l/5VIp6Ww==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5bTVkK0pQL3kxZzdaN0xq
YU9rVTNvQXE1V1d0Ykx1bEN3cXp4eWdKaWpjCkEzNUs0N1JlMjlrdnA4UUxIRkZ1
aFl2Qk9PeTNTQ0grMjFJMzZvT05adGMKLS0tIEIwd3NyQ0xWWkVBMFl4Z0VUVFBY
eWFlSFYrL1E1aXN2bVp5YTF5VXVOYUEK3NhyHGnY+a/Fj6TJFzljSX+49G9x5F3x
M16Nx7JGsD5IV6GfdiMYSEwgT1pm0/XY8rhvWBO1fArkleMz1HKewg==
-----END AGE ENCRYPTED FILE-----
recipient: age1p4gyh5260ewp7t2ctzv5ewj4a06szl389fm4gzy6vltxhjj73ers87u33g
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwWGpwUkRueVhhcVErQ0ZF
RXdxV1lMNzh6VFhubmc3TDBMaSsrYjhlS1Q0CnlZMmJSaXN5d1daRmpUVTlWeU1i
Nkxsd3JoTGpoelhiTXBMclhoL3RKQlEKLS0tIHRyQjk0akN3WnhKKy82T0ZOYTNK
QU5tYkZuTVc2Yzc0bzJrNXd1aWJMVlEKoMHFqDQYmT+q4eSNtmIb6drI/pFt9Z4W
gHI/QxeWQq6BWssk4EAonRXhIE0iJPOsXz2zyT+dRRDqcKBod9h01w==
-----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
+1
View File
@@ -14,6 +14,7 @@
proton-vpn proton-vpn
joplin-desktop joplin-desktop
discord discord
github-copilot-cli
]; ];
}; };
} }
-445
View File
@@ -1,445 +0,0 @@
{ self, inputs, lib, ... }:
let
# Options that will be in common between the nixos module and the home-manager module.
mkOpts = config: let cfg = config.mtls; in {
enable = lib.mkEnableOption "Enable mTLS";
subject = lib.mkOption {
description = "The Common Name, DNS Name, or IP address that will be set as the Subject Common Name for the certificate. If no Subject Alternative Names (SANs) are configured (via the --san flag) then the subject will be set as the only SAN.";
type = lib.types.str;
};
certDir = lib.mkOption {
description = "String path to the directory where the certs will be stored";
type = lib.types.str;
};
caFile = lib.mkOption {
description = "String path for the root CA file";
type = lib.types.str;
default = "${cfg.certDir}/root_ca.crt";
};
keyFile = lib.mkOption {
description = "String path for the private key";
type = lib.types.str;
default = "${cfg.certDir}/key.pem";
};
certFile = lib.mkOption {
description = "String path for the public cert";
type = lib.types.str;
default = "${cfg.certDir}/cert.pem";
};
bundleFile = lib.mkOption {
description = "String path for the mTLS key bundle";
type = lib.types.str;
default = "${cfg.certDir}/mtls.pem";
};
san = lib.mkOption {
description = "List of SAN to give the mTLS cert";
type = lib.types.listOf lib.types.str;
default = [ ];
};
provisioner = lib.mkOption {
type = lib.types.str;
default = "admin";
};
lifetime = lib.mkOption {
type = lib.types.str;
default = "24h";
};
renew = {
enable = lib.mkOption {
description = "Enable automatic mTLS certificate renewal using a systemd timer.";
type = lib.types.bool;
default = cfg.enable;
};
onCalendar = lib.mkOption {
description = "systemd OnCalendar schedule for mTLS certificate renewal checks.";
type = lib.types.str;
default = "*:1/15";
};
randomizedDelaySec = lib.mkOption {
description = "Randomized delay added to renewal timer runs to avoid synchronized renewals.";
type = lib.types.str;
default = "5m";
};
user = lib.mkOption {
description = "User account to run the mTLS renewal service as.";
type = lib.types.str;
default = "root";
};
group = lib.mkOption {
description = "Group to run the mTLS renewal service as. Defaults to the configured renewal user.";
type = lib.types.nullOr lib.types.str;
default = "mtls";
};
reloadUnits = lib.mkOption {
description = "systemd units to try-reload-or-restart after a successful certificate renewal.";
type = lib.types.listOf lib.types.str;
default = [ ];
};
postCommands = lib.mkOption {
description = "Shell commands to run after a successful certificate renewal.";
type = lib.types.listOf lib.types.lines;
default = [ ];
};
};
};
mkMtlsGenerateScript = {
pkgs,
subject,
provisioner,
san,
certFile,
keyFile,
bundleFile,
lifetime,
user,
group,
}:
let
sanArgs = lib.concatMapStringsSep " " (s: "--san \"${s}\"") san;
in
pkgs.writeShellApplication {
name = "mtls-generate";
runtimeInputs = with pkgs; [ coreutils step-cli ];
text = ''
set -euo pipefail
step ca certificate ${subject} ${certFile} ${keyFile} \
--provisioner ${provisioner} \
--not-before=-5m --not-after=${lifetime} \
${sanArgs} \
"$@"
(umask 077; cat ${certFile} ${keyFile} > ${bundleFile})
chown ${user}:${group} ${certFile} ${keyFile} ${bundleFile}
chmod 640 ${certFile} ${keyFile} ${bundleFile}
printf '\033[32m\033[0m \033[1mmTLS Bundle:\033[0m %s\n' ${lib.escapeShellArg bundleFile}
'';
};
in
{
flake.modules.nixos.mtls = { config, lib, pkgs, ... }:
let
cfg = config.mtls;
mtlsRenewWrapper = inputs.self.wrappers.mtlsRenew.apply {
inherit pkgs;
inherit (cfg) certDir keyFile certFile bundleFile;
inherit (cfg.renew) user group reloadUnits postCommands;
systemd = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
User = cfg.renew.user;
Group = cfg.renew.group;
};
} // lib.optionalAttrs cfg.renew.enable {
startAt = cfg.renew.onCalendar;
};
};
in
{
options.mtls = (mkOpts config) // {
certDir = lib.mkOption {
description = "String path to where the mtls certs will be stored.";
type = lib.types.str;
default = "/etc/step-ca/certs";
};
certReaders = lib.mkOption {
description = "";
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
config = lib.mkIf cfg.enable {
users.groups.certReaders = {
name = cfg.renew.group;
members = cfg.certReaders;
};
environment.systemPackages = with pkgs; lib.optionals cfg.enable [
# step-cli
(mkMtlsGenerateScript {
inherit pkgs;
inherit (cfg) subject provisioner san certFile keyFile bundleFile lifetime;
inherit (cfg.renew) user group;
})
(inputs.self.wrappers.mtlsCheck.apply {
inherit pkgs;
inherit (cfg) bundleFile;
}).wrapper
mtlsRenewWrapper.wrapper
];
systemd.tmpfiles.rules = [
"d ${cfg.certDir} 0750 ${cfg.renew.user} ${cfg.renew.group} -"
];
systemd.packages = lib.mkIf cfg.renew.enable [
mtlsRenewWrapper.outputs.systemd-system
];
systemd.timers.mtls-renew = lib.mkIf cfg.renew.enable {
wantedBy = [ "timers.target" ];
timerConfig = {
Persistent = true;
AccuracySec = "1us";
RandomizedDelaySec = cfg.renew.randomizedDelaySec;
};
};
};
};
flake.modules.homeManager.mtls = { config, lib, pkgs, ... }:
let
cfg = config.mtls;
mtlsRenewWrapper = inputs.self.wrappers.mtlsRenew.apply {
inherit pkgs;
inherit (cfg) certDir keyFile certFile bundleFile;
inherit (cfg.renew) reloadUnits postCommands;
systemctlArgs = [ "--user" ];
systemd = lib.optionalAttrs cfg.renew.enable {
startAt = cfg.renew.onCalendar;
};
};
in
{
options.mtls = (mkOpts config) // {
certDir = lib.mkOption {
description = "String path to where the mtls certs will be stored.";
type = lib.types.str;
default = "${config.home.homeDirectory}/.step/certs";
};
};
config = {
home.packages = with pkgs; lib.optionals cfg.enable [
# step-cli
(mkMtlsGenerateScript {
inherit pkgs;
inherit (cfg) keyFile certFile bundleFile;
inherit (cfg) subject provisioner san lifetime;
inherit (cfg.renew) user group;
})
(inputs.self.wrappers.mtlsCheck.apply {
inherit pkgs;
inherit (cfg) bundleFile;
}).wrapper
];
systemd.user.tmpfiles.rules = lib.mkIf cfg.enable [
"d ${cfg.certDir} 0700 - - -" # Ensure the cert directory exists and is writable by the user
];
# Create the systemd service files for the user.
xdg.configFile = lib.mkIf cfg.renew.enable {
"systemd/user/mtls-renew.service".source =
"${mtlsRenewWrapper.outputs.systemd-user}/systemd/user/mtls-renew.service";
"systemd/user/mtls-renew.timer".source =
"${mtlsRenewWrapper.outputs.systemd-user}/systemd/user/mtls-renew.timer";
"systemd/user/mtls-renew.timer.d/override.conf".text = ''
[Timer]
Persistent=true
AccuracySec=1us
RandomizedDelaySec=${cfg.renew.randomizedDelaySec}
'';
};
# Ensure the timer gets started
home.activation.mtlsRenewTimer = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
if [ -n "$XDG_RUNTIME_DIR" ] && [ -S "$XDG_RUNTIME_DIR/systemd/private" ]; then
if [ "${lib.boolToString (cfg.enable && cfg.renew.enable)}" = "true" ]; then
run ${pkgs.systemd}/bin/systemctl --user daemon-reload
if ${pkgs.systemd}/bin/systemctl --user cat mtls-renew.timer >/dev/null 2>&1; then
run ${pkgs.systemd}/bin/systemctl --user enable --now mtls-renew.timer
else
verboseEcho "mtls-renew.timer unit file is not available; skipping enable"
fi
else
run ${pkgs.systemd}/bin/systemctl --user disable --now mtls-renew.timer || true
run ${pkgs.systemd}/bin/systemctl --user daemon-reload || true
fi
fi
'';
};
};
flake.wrappers = {
mtlsCheck = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
options = {
bundleFile = lib.mkOption {
description = "String path for the mTLS key bundle";
type = lib.types.str;
};
};
config = {
binName = "mtls-check";
# This pattern is necessary to wrap packages like openssl that provide more than one binary
package = config.pkgs.symlinkJoin {
name = "openssl";
paths = [ config.pkgs.openssl.bin config.pkgs.openssl.man ];
meta.mainProgram = "openssl";
};
args = [
"x509"
"-noout"
"-in" config.bundleFile
"-subject"
"-issuer"
"-ext" "subjectAltName,extendedKeyUsage"
"-enddate"
];
};
});
mtlsNeedsRenewal = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
options = {
certFile = lib.mkOption {
description = "String path for the public cert";
type = lib.types.str;
};
};
config = {
binName = "mtls-needs-renewal";
package = config.pkgs.step-cli;
preHook = ''
echo "Checking renewal status..."
'';
args = [ "certificate" "needs-renewal" "${config.certFile}" ];
};
});
mtlsRenew = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
imports = [ wlib.modules.systemd ];
options = {
certDir = lib.mkOption {
description = "String path to the directory where the certs will be stored";
type = lib.types.str;
};
keyFile = lib.mkOption {
description = "String path for the private key";
type = lib.types.str;
default = "${config.certDir}/key.pem";
};
certFile = lib.mkOption {
description = "String path for the public cert";
type = lib.types.str;
default = "${config.certDir}/cert.pem";
};
bundleFile = lib.mkOption {
description = "String path for the mTLS key bundle";
type = lib.types.str;
default = "${config.certDir}/mtls.pem";
};
user = lib.mkOption {
description = "User that should own the renewed certificate files.";
type = lib.types.nullOr lib.types.str;
default = null;
};
group = lib.mkOption {
description = "Group that should own the renewed certificate files.";
type = lib.types.nullOr lib.types.str;
default = null;
};
reloadUnits = lib.mkOption {
description = "systemd units to try-reload-or-restart after a successful renewal.";
type = lib.types.listOf lib.types.str;
default = [ ];
};
postCommands = lib.mkOption {
description = "Shell commands to run after a successful renewal.";
type = lib.types.listOf lib.types.lines;
default = [ ];
};
systemctlArgs = lib.mkOption {
description = "Additional arguments to pass to systemctl when reloading units.";
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
config = {
binName = "mtls-renew";
package = let
systemctlCmd = "systemctl ${lib.escapeShellArgs config.systemctlArgs}";
hasReloadUnits = config.reloadUnits != [ ];
renewReloadScript = lib.concatMapStringsSep "\n" (unit: ''
if ${systemctlCmd} --quiet is-active "${unit}"; then
${systemctlCmd} try-reload-or-restart "${unit}"
fi
'') config.reloadUnits;
hasPostCommands = config.postCommands != [ ];
renewPostCommands = lib.concatStringsSep "\n" config.postCommands;
hasOwnership = config.user != null && config.group != null;
in
config.pkgs.writeShellApplication {
name = "mtls-renew";
runtimeInputs = with config.pkgs; [ coreutils step-cli systemd ];
text = ''
set -euo pipefail
YELLOW_BANG="\e[33m!\e[0m"
force=0
while [[ $# -gt 0 ]]; do
case $1 in
--force)
force=1
shift
;;
*)
echo -e "$YELLOW_BANG Warning: ignoring unrecognized argument '$1'"
exit 1
;;
esac
done
if [[ $force -eq 0 ]] && ! step certificate needs-renewal "${config.certFile}"; then
echo "Skipping renew"
exit 0
fi
echo "Renewing mTLS certificate"
step ca renew --force "${config.certFile}" "${config.keyFile}"
(umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}")
${lib.optionalString hasOwnership ''
chown ${config.user}:${config.group} ${config.certFile} ${config.keyFile} ${config.bundleFile}
chmod 640 ${config.certFile} ${config.keyFile} ${config.bundleFile}
''}
${lib.optionalString hasReloadUnits ''
echo "Reloading units: ${lib.concatStringsSep ", " config.reloadUnits}"
${renewReloadScript}
''}
${lib.optionalString hasPostCommands ''
echo "Running post commands"
${renewPostCommands}
''}
'';
};
extraPackages = [
config.pkgs.step-cli
];
systemd = {
description = "Renew the mTLS certificate when Smallstep marks it ready";
documentation = [
"https://smallstep.com/docs/step-ca/certificate-authority-server-production"
];
startLimitIntervalSec = 0;
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig.Type = lib.mkDefault "oneshot";
# serviceConfig.ExecCondition = "";
};
};
});
};
}
+49
View File
@@ -0,0 +1,49 @@
{ lib, ... }: {
options.optionModules.mtls = lib.mkOption {
type = lib.types.deferredModule;
description = "Shared mTLS certificate option definitions, imported by the mTLS wrapper modules.";
};
config.optionModules.mtls = { config, lib, pkgs, ... }: {
key = "mtls-config";
_file = "modules/features/mtls/config.nix";
options = {
certDir = lib.mkOption {
description = "String path to the directory where the certs will be stored";
type = lib.types.str;
default = "/etc/mtls";
};
keyFile = lib.mkOption {
description = "String path for the private key";
type = lib.types.str;
default = "${config.certDir}/key.pem";
};
certFile = lib.mkOption {
description = "String path for the public cert";
type = lib.types.str;
default = "${config.certDir}/cert.pem";
};
bundleFile = lib.mkOption {
description = "String path for the mTLS key bundle";
type = lib.types.str;
default = "${config.certDir}/mtls.pem";
};
subject = lib.mkOption {
description = "Subject for the cert";
type = lib.types.str;
};
provisioner = lib.mkOption {
type = lib.types.nullOr lib.types.str;
};
provisionerPasswordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
};
overwrite = lib.mkEnableOption "Overwrite existing cert file?";
SANs = lib.mkOption {
description = "A list of Subject Alternative Names";
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
};
}
+167
View File
@@ -0,0 +1,167 @@
{ self, inputs, lib, ... }:
let
# Options that will be in common between the nixos module and the home-manager module.
mkOpts = config: let cfg = config.mtls; in {
enable = lib.mkEnableOption "Enable mTLS";
subject = lib.mkOption {
description = "The Common Name, DNS Name, or IP address that will be set as the Subject Common Name for the certificate. If no Subject Alternative Names (SANs) are configured (via the --san flag) then the subject will be set as the only SAN.";
type = lib.types.str;
};
certDir = lib.mkOption {
description = "String path to the directory where the certs will be stored";
type = lib.types.str;
};
caFile = lib.mkOption {
description = "String path for the root CA file";
type = lib.types.str;
default = "${cfg.certDir}/root_ca.crt";
};
keyFile = lib.mkOption {
description = "String path for the private key";
type = lib.types.str;
default = "${cfg.certDir}/key.pem";
};
certFile = lib.mkOption {
description = "String path for the public cert";
type = lib.types.str;
default = "${cfg.certDir}/cert.pem";
};
bundleFile = lib.mkOption {
description = "String path for the mTLS key bundle";
type = lib.types.str;
default = "${cfg.certDir}/mtls.pem";
};
san = lib.mkOption {
description = "List of SAN to give the mTLS cert";
type = lib.types.listOf lib.types.str;
default = [ ];
};
provisioner = lib.mkOption {
type = lib.types.str;
default = "admin";
};
lifetime = lib.mkOption {
type = lib.types.str;
default = "24h";
};
renew = {
enable = lib.mkOption {
description = "Enable automatic mTLS certificate renewal using a systemd timer.";
type = lib.types.bool;
default = cfg.enable;
};
onCalendar = lib.mkOption {
description = "systemd OnCalendar schedule for mTLS certificate renewal checks.";
type = lib.types.str;
default = "*:1/15";
};
randomizedDelaySec = lib.mkOption {
description = "Randomized delay added to renewal timer runs to avoid synchronized renewals.";
type = lib.types.str;
default = "5m";
};
user = lib.mkOption {
description = "User account to run the mTLS renewal service as.";
type = lib.types.str;
default = "root";
};
group = lib.mkOption {
description = "Group to run the mTLS renewal service as. Defaults to the configured renewal user.";
type = lib.types.nullOr lib.types.str;
default = "mtls";
};
reloadUnits = lib.mkOption {
description = "systemd units to try-reload-or-restart after a successful certificate renewal.";
type = lib.types.listOf lib.types.str;
default = [ ];
};
postCommands = lib.mkOption {
description = "Shell commands to run after a successful certificate renewal.";
type = lib.types.listOf lib.types.lines;
default = [ ];
};
};
};
in
{
flake.modules.nixos.mtls = { config, lib, pkgs, ... }:
let
cfg = config.mtls;
mtlsWrappers = inputs.self.wrappers.mtls;
mtlsGenerate = mtlsWrappers.generate.apply {
inherit pkgs;
inherit (cfg) subject;
SANs = cfg.san;
provisioner = "admin";
provisionerPasswordFile = config.sops.secrets."janus/admin_jwk".path;
};
mtlsRenew = mtlsWrappers.renew.apply {
inherit pkgs;
};
mtlsCheck = mtlsWrappers.check.apply {
inherit pkgs;
};
in
{
options.mtls = (mkOpts config) // {
certDir = lib.mkOption {
description = "String path to where the mtls certs will be stored.";
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;
default = [ ];
};
};
config = lib.mkIf cfg.enable {
users.groups.certReaders = {
name = cfg.renew.group;
members = cfg.certReaders;
};
environment.systemPackages = [
mtlsGenerate.wrapper
mtlsCheck.wrapper
mtlsRenew.wrapper
];
systemd = {
packages = [ mtlsRenew.outputs.systemd-system ];
# Timer-driven oneshot: only the timer is enabled. NixOS does not
# honor the unit's [Install] section for systemd.packages, so the
# wantedBy must be set explicitly here.
timers.mtls-renew.wantedBy = [ "timers.target" ];
};
};
};
}
+90
View File
@@ -0,0 +1,90 @@
{ self, inputs, lib, config, ... }:
let
mtlsConfigModule = config.optionModules.mtls;
mkSANArgs = sans: builtins.concatLists (map (name: [ "--san" name ]) sans);
in
{
flake.wrappers.mtls = {
generate = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
imports = [ mtlsConfigModule ];
config = {
binName = "mtls-generate";
package = config.pkgs.step-cli;
extraPackages = with config.pkgs; [ coreutils step-cli systemd ];
preHook = "mkdir -p ${config.certDir}";
args = [
"ca" "certificate"
"${config.subject}" "${config.certFile}" "${config.keyFile}"
"--not-before" "-5m"
"--not-after" "24h"
]
++ lib.optionals (config.provisioner != null) [ "--provisioner" "${config.provisioner}" ]
++ lib.optionals (config.provisionerPasswordFile != null) [
"--provisioner-password-file" "${config.provisionerPasswordFile}"
]
++ lib.optionals config.overwrite [ "-f" ]
++ mkSANArgs config.SANs;
postHook = ''
(umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}")
'';
};
});
renew = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
# https://github.com/Lassulus/wrappers#generating-systemd-services
imports = [
wlib.modules.systemd mtlsConfigModule
mtlsConfigModule
];
config = {
binName = "mtls-renew";
package = config.pkgs.step-cli;
extraPackages = with config.pkgs; [ coreutils step-cli systemd ];
args = [
"ca" "renew"
"${config.certFile}" "${config.keyFile}"
];
postHook = ''
(umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}")
'';
systemd = {
description = "Renew the mTLS certificate when Smallstep marks it ready";
documentation = [
"https://smallstep.com/docs/step-ca/certificate-authority-server-production"
];
startLimitIntervalSec = 0;
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = lib.mkDefault "oneshot";
ExecCondition = "${lib.getExe config.pkgs.step-cli} certificate needs-renewal ${config.certFile}";
};
startAt = "hourly";
};
};
});
check = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
imports = [ mtlsConfigModule ];
config = {
binName = "mtls-check";
# This pattern is necessary to wrap packages like openssl that provide more than one binary
package = config.pkgs.symlinkJoin {
name = "openssl";
paths = [ config.pkgs.openssl.bin config.pkgs.openssl.man ];
meta.mainProgram = "openssl";
};
args = [
"x509"
"-noout"
"-in" config.bundleFile
"-subject"
"-issuer"
"-ext" "subjectAltName,extendedKeyUsage"
"-enddate"
];
};
});
};
}
-100
View File
@@ -1,100 +0,0 @@
{ self, inputs, ... }:
let
mkPrincipalArgs = principals:
builtins.concatLists (map (principal: [ "--principal" principal ]) principals);
in
{
perSystem = { system, self', pkgs, lib, ... }: {
packages.ssh-certs = inputs.wrappers.lib.wrapPackage {
inherit pkgs;
package = (pkgs.symlinkJoin {
name = "ssh-certs";
meta.mainProgram = "sign-ssh-user-cert";
paths = [
(inputs.self.wrappers.signUserWrapper.apply {
inherit pkgs;
provisioner = "admin";
overwrite = true;
validUsers = [ "john" "root" "appdaemon" ];
}).wrapper
(inputs.self.wrappers.signHostWrapper.apply {
inherit pkgs;
provisioner = "admin";
overwrite = true;
# extraPrincipals = [ "home-pc" ];
}).wrapper
];
});
};
};
flake.wrappers.signHostWrapper = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
options = {
provisioner = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "admin";
};
extraPrincipals = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
overwrite = lib.mkEnableOption "Overwrite existing cert file?";
};
config = {
binName = "sign-ssh-host-cert";
package = config.pkgs.step-cli;
extraPackages = with config.pkgs; [ hostname iproute2 systemd ];
preHook = ''
HOSTNAME=$(hostname -s)
IP_ADDRESS=$(ip -4 -o addr show scope global | while read -r _ _ _ addr _; do
case "$addr" in
192.168.1.*/*)
printf '%s\n' "''${addr%%/*}"
break
;;
esac
done)
echo "Signing SSH host cert for $HOSTNAME at $IP_ADDRESS"
'';
args =
[
"ssh" "certificate"
"--host" "--sign"
"--principal" "$HOSTNAME"
"--principal" "$IP_ADDRESS"
]
++ lib.optionals (config.provisioner != null) [ "--provisioner" "${config.provisioner}" ]
++ lib.optionals config.overwrite [ "-f" ]
++ mkPrincipalArgs config.extraPrincipals;
postHook = ''
systemctl reload-or-restart sshd
'';
};
});
flake.wrappers.signUserWrapper = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
options = {
provisioner = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "admin";
};
validUsers = lib.mkOption {
description = "A list of the user names that this cert will be valid for";
type = lib.types.listOf lib.types.str;
default = [ ];
};
overwrite = lib.mkEnableOption "Overwrite existing cert file?";
};
config = {
binName = "sign-ssh-user-cert";
package = config.pkgs.step-cli;
args = [ "ssh" "certificate" "--sign" ]
++ lib.optionals (config.provisioner != null) [ "--provisioner" "${config.provisioner}" ]
++ lib.optionals config.overwrite [ "-f" ]
++ mkPrincipalArgs config.validUsers;
};
});
}
+79
View File
@@ -0,0 +1,79 @@
{ lib, ... }: {
options.optionModules.ssh-certs = lib.mkOption {
type = lib.types.deferredModule;
description = "SSH certificate options";
};
config.optionModules.ssh-certs = { config, lib, pkgs, ... }: {
# Needed for some kind of de-duping when this module is used more than once?
key = "ssh-cert-config";
_file = "modules/features/ssh/config.nix";
options.ssh-new = {
user = {
keyFile = lib.mkOption {
type = lib.types.str;
default = "id_ed25519";
};
};
host = {
configDir = lib.mkOption {
type = lib.types.str;
default = "/etc/ssh";
};
keyFile = lib.mkOption {
description = "String path to the host private key file";
type = lib.types.str;
default = "ssh_host_ed25519_key";
};
keyType = lib.mkOption {
description = "OpenSSH host key type for ssh.hostKey.";
type = lib.types.enum [ "ed25519" "rsa" "ecdsa" ];
default = "ed25519";
};
extraSettings = lib.mkOption {
description = "Extra settings to merge";
type = lib.types.attrs;
default = { };
};
enable-scripts = lib.mkEnableOption "Enable SSH host cert management scripts";
};
certificates = {
provisioner = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
provisionerPasswordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
overwrite = lib.mkEnableOption "Overwrite existing certificate files";
user = {
enable = lib.mkEnableOption "Enable SSH user certs";
CAFile = lib.mkOption {
description = "Filename of the SSH user CA with the config directory";
type = lib.types.str;
default = "ssh_user_ca_key.pub";
};
extraPrincipals = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "root" "john" "appdaemon" ];
};
};
host = {
enable = lib.mkEnableOption "Enable SSH host certs";
autoRenew = lib.mkEnableOption "Auto-renew the SSH host certs with a systemd service/timer";
extraPrincipals = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
expires-in = lib.mkOption {
description = "Duration passed to step ssh needs-renewal --expires-in.";
type = lib.types.str;
default = "4h";
};
};
};
};
};
}
+258
View File
@@ -0,0 +1,258 @@
# New attempt at consolidating SSH config that wraps the step client, SSH certs, and all that in a top-level module
{ self, inputs, lib, config, ... }:
let
sshHostCAPath = ../../hosts/janus/public/ssh_host_ca_key.pub;
caPatterns = [ "*.john-stream.com" "192.168.1.*" "fded:fb16:653e:25da:be24:11ff:*" ];
wrappers = inputs.self.wrappers;
sshCertConfig = config.optionModules.ssh-certs;
mkHostScripts = { cfg, pkgs, provisionerPasswordFile ? null }: let
hostKeyFile = "${cfg.host.configDir}/${cfg.host.keyFile}";
hostCertFile = "${hostKeyFile}-cert.pub";
wrapperCfg = cfg // {
certificates = cfg.certificates // {
provisionerPasswordFile = provisionerPasswordFile;
overwrite = true;
};
};
in {
inherit hostKeyFile hostCertFile;
sign = (wrappers.signHostWrapper.apply {
inherit pkgs;
ssh-new = wrapperCfg;
}).wrapper;
renew = (wrappers.renewHostWrapper.apply {
inherit pkgs;
ssh-new = wrapperCfg;
}).wrapper;
check = (wrappers.hostCheckWrapper.apply {
inherit pkgs;
ssh-new = wrapperCfg;
}).wrapper;
renewalCheck = (wrappers.renewalCheck.apply {
inherit pkgs;
ssh-new = wrapperCfg;
}).wrapper;
};
mkUserScripts = { cfg, pkgs, provisionerPasswordFile ? null }: {
sign = let
wrapperCfg = cfg // {
certificates = cfg.certificates // {
provisionerPasswordFile = provisionerPasswordFile;
overwrite = true;
};
};
in (wrappers.signUserWrapper.apply {
inherit pkgs;
ssh-new = wrapperCfg;
}).wrapper;
check = (wrappers.userCheckWrapper.apply {
inherit pkgs;
ssh-new = cfg;
}).wrapper;
};
in
{
flake.modules.nixos.ssh-new = { config, pkgs, lib, ... }:
let
cfg = config.ssh-new;
hasAdminJwk = lib.hasAttrByPath [ "sops" "secrets" "janus/admin_jwk" ] config;
adminJwkPath = if hasAdminJwk then config.sops.secrets."janus/admin_jwk".path else null;
hostScripts = mkHostScripts {
inherit cfg pkgs;
provisionerPasswordFile = if cfg.certificates.host.autoRenew
then adminJwkPath
else null;
};
CAknownHosts = (lib.genAttrs caPatterns (_: {
certAuthority = true;
publicKey = lib.removeSuffix "\n" (builtins.readFile sshHostCAPath);
}));
in
{
imports = [ sshCertConfig ];
config = {
services.openssh = {
enable = true;
openFirewall = true;
hostKeys = [
{
path = hostScripts.hostKeyFile;
type = cfg.host.keyType;
}
];
settings = lib.mkMerge [
{
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
}
cfg.host.extraSettings
(lib.mkIf cfg.certificates.host.enable {
HostCertificate = hostScripts.hostCertFile;
})
(lib.mkIf cfg.certificates.user.enable {
TrustedUserCAKeys = "${cfg.host.configDir}/${cfg.certificates.user.CAFile}";
})
];
};
environment.etc."ssh/${cfg.certificates.user.CAFile}" = lib.mkIf cfg.certificates.user.enable {
source = ../../hosts/janus/public/ssh_user_ca_key.pub;
};
environment.systemPackages = (
lib.optionals cfg.certificates.host.enable [
hostScripts.sign
hostScripts.renew
hostScripts.renewalCheck
hostScripts.check
]
);
sops = lib.mkIf cfg.certificates.host.autoRenew {
secrets."janus/admin_jwk" = {
sopsFile = ../../../keys/secrets.yaml;
owner = "root";
group = "root";
mode = "0400";
};
};
systemd = lib.mkIf cfg.certificates.host.autoRenew {
services.ssh-certs-renew = {
description = "SSH host certificate renewal";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = with pkgs; [ step-cli systemd ];
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
ExecCondition = lib.getExe hostScripts.renewalCheck;
ExecStart = lib.getExe hostScripts.renew;
};
};
timers.ssh-certs-renew = {
description = "Periodic Step SSH host certificate renewal";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5m";
OnUnitActiveSec = "4h";
RandomizedDelaySec = "15m";
Persistent = true;
Unit = "ssh-certs-renew.service";
};
};
};
# This winds up in /etc/ssh/ssh_known_hosts, which applies to all users
programs.ssh.knownHosts = lib.mkIf cfg.certificates.user.enable CAknownHosts;
home-manager.users.root = lib.mkIf cfg.certificates.user.enable {
home.stateVersion = lib.mkDefault config.system.stateVersion;
imports = [ inputs.self.modules.homeManager.ssh-new ];
ssh-new.certificates.user.enable = true;
};
};
};
flake.modules.homeManager.ssh-new = { config, pkgs, lib, ... }:
let
cfg = config.ssh-new;
userKeyPath =
if lib.hasPrefix "/" cfg.user.keyFile || lib.hasPrefix "~/" cfg.user.keyFile
then cfg.user.keyFile
else "${config.home.homeDirectory}/.ssh/${cfg.user.keyFile}";
hasAdminJwk = lib.hasAttrByPath [ "sops" "secrets" "janus/admin_jwk" ] config;
adminJwkPath = if hasAdminJwk then config.sops.secrets."janus/admin_jwk".path else null;
hostScripts = mkHostScripts { inherit cfg pkgs; };
sshHostCAContent = lib.removeSuffix "\n" (builtins.readFile sshHostCAPath);
knownHostsText = lib.concatMapStrings
(pattern: "@cert-authority ${pattern} ${sshHostCAContent}\n")
caPatterns;
userScripts = mkUserScripts {
inherit cfg pkgs;
provisionerPasswordFile = adminJwkPath;
};
in
{
imports = [ sshCertConfig ];
config = {
home.file.".ssh/known_hosts" = lib.mkIf cfg.certificates.user.enable {
text = knownHostsText;
force = true;
};
home.packages = lib.optionals cfg.certificates.user.enable [
userScripts.sign
userScripts.check
];
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
# These settings apply to all connections
"*" = lib.mkMerge (
[
# Default settings
{
Compression = false;
ServerAliveInterval = 60;
ServerAliveCountMax = 3;
TCPKeepAlive = "yes";
ConnectTimeout = 3;
PubkeyAuthentication = "yes";
PasswordAuthentication = "no";
PreferredAuthentications = "publickey";
IdentitiesOnly = true;
IdentityFile = userKeyPath;
StrictHostKeyChecking = "accept-new";
UserKnownHostsFile = "${config.home.homeDirectory}/.ssh/known_hosts";
HashKnownHosts = lib.mkDefault false;
AddKeysToAgent = lib.mkDefault "yes";
ForwardAgent = lib.mkDefault false;
RequestTTY = lib.mkDefault "auto";
SetEnv.TERM = "xterm-256color";
}
]
# SSH certificate settings
++ lib.optionals cfg.certificates.user.enable [
{
CertificateFile = "${userKeyPath}-cert.pub";
}
]
);
"john-pc" = {
HostName = "192.168.1.85";
User = "john";
};
"gitea" = {
HostName = "192.168.1.104";
User = "john";
};
"janus" = {
HostName = "fded:fb16:653e:25da:be24:11ff:fe6b:4d57";
User = "root";
};
"soteria" = {
HostName = "fded:fb16:653e:25da:be24:11ff:fe54:aa39";
User = "root";
};
"hermes" = {
HostName = "192.168.1.150";
User = "root";
};
"jdl-docker" = {
HostName = "jdl-docker.tailcf205.ts.net";
User = "john";
};
};
};
};
};
}
+121
View File
@@ -0,0 +1,121 @@
{ self, inputs, config, ... }:
let
sshCertConfig = config.optionModules.ssh-certs;
mkHostKeyFile = cfg: "${cfg.host.configDir}/${cfg.host.keyFile}";
mkPrincipalArgs = principals:
builtins.concatLists (map (principal: [ "--principal" principal ]) principals);
in
{
flake.wrappers.signHostWrapper = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
imports = [ sshCertConfig ];
config = let cfg = config.ssh-new; in {
binName = "ssh-host-cert-sign";
package = config.pkgs.step-cli;
extraPackages = with config.pkgs; [ hostname iproute2 systemd ];
preHook = ''
HOSTNAME=$(hostname -s)
IP_ADDRESS=$(ip -4 -o addr show scope global | while read -r _ _ _ addr _; do
case "$addr" in
192.168.1.*/*)
printf '%s\n' "''${addr%%/*}"
break
;;
esac
done)
echo "Signing SSH host cert for $HOSTNAME at $IP_ADDRESS"
'';
args =
[
"ssh" "certificate"
"--host" "--sign"
"--principal" "$HOSTNAME"
"--principal" "$IP_ADDRESS"
]
++ lib.optionals (cfg.certificates.provisioner != null) [
"--provisioner" "${cfg.certificates.provisioner}"
]
++ lib.optionals (cfg.certificates.provisionerPasswordFile != null) [
"--provisioner-password-file" "${cfg.certificates.provisionerPasswordFile}"
]
++ lib.optionals cfg.certificates.overwrite [ "-f" ]
++ mkPrincipalArgs cfg.certificates.host.extraPrincipals;
};
});
flake.wrappers.renewHostWrapper = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
imports = [ sshCertConfig ];
config = {
binName = "ssh-host-cert-renew";
package = config.pkgs.step-cli;
extraPackages = with config.pkgs; [ systemd ];
args =
let hostKeyFile = mkHostKeyFile config.ssh-new;
in [ "ssh" "renew" "${hostKeyFile}-cert.pub" "${hostKeyFile}" ]
++ lib.optionals config.ssh-new.certificates.overwrite [ "-f" ];
};
});
flake.wrappers.hostCheckWrapper = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
imports = [ sshCertConfig ];
config = {
binName = "ssh-host-cert-check";
package = config.pkgs.openssh;
exePath = lib.getExe' config.pkgs.openssh "ssh-keygen";
args = [ "-Lf" "${mkHostKeyFile config.ssh-new}-cert.pub" ];
};
});
flake.wrappers.signUserWrapper = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
imports = [ sshCertConfig ];
config =
let
cfg = config.ssh-new;
in {
binName = "ssh-user-cert-sign";
package = config.pkgs.step-cli;
args = [ "ssh" "certificate" "--sign" ]
++ lib.optionals (cfg.certificates.provisioner != null) [
"--provisioner" "${cfg.certificates.provisioner}"
]
++ lib.optionals (cfg.certificates.provisionerPasswordFile != null) [
"--provisioner-password-file" "${cfg.certificates.provisionerPasswordFile}"
]
++ lib.optionals cfg.certificates.overwrite [ "-f" ]
++ mkPrincipalArgs cfg.certificates.user.extraPrincipals;
};
});
flake.wrappers.userCheckWrapper = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
imports = [ sshCertConfig ];
config =
let
cfg = config.ssh-new;
in {
binName = "ssh-user-cert-check";
package = config.pkgs.openssh;
exePath = lib.getExe' config.pkgs.openssh "ssh-keygen";
args = [ "-Lf" "$HOME/.ssh/${config.ssh-new.user.keyFile}-cert.pub" ];
};
});
flake.wrappers.renewalCheck = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
imports = [ sshCertConfig ];
config =
let
cfg = config.ssh-new;
hostKeyFile = mkHostKeyFile cfg;
hostCertFile = "${hostKeyFile}-cert.pub";
in
{
binName = "ssh-renewal-check";
package = config.pkgs.step-cli;
preHook = ''
echo "Checking SSH cert at ${hostCertFile}"
'';
args = [
"ssh" "needs-renewal" hostCertFile
"--expires-in" "${cfg.certificates.host.expires-in}"
];
};
});
}
+138 -21
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/public/root_ca.crt;
root = ../hosts/janus/root_ca.crt;
}; mkStepBootstrap = { pkgs, caUrl, fingerprint, install ? false }:
home.packages = [ (inputs.self.wrappers.stepBootstrap.apply {
inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.step-bootstrap inherit pkgs install;
]; ca-url = caUrl;
# sops.secrets."step-ca-defaults" = { inherit fingerprint;
# sopsFile = ../hosts/janus/defaults.json; }).wrapper;
# format = "json";
# key = ""; # This causes it to decode the whole file mkDefaultsText = cfg: builtins.toJSON {
# path = "${config.home.homeDirectory}/defaults.json"; ca-url = cfg.caUrl;
# mode = "0400"; fingerprint = cfg.fingerprint;
# }; root = cfg.root;
}; };
in
{
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;
})
];
environment.etc."step-ca/defaults.json".text = mkDefaultsText cfg;
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 -"
"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 janus@john-stream.com -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 across hosts
Then update public artifacts in repo from generated output:
- `modules/hosts/janus/public/root_ca.crt` from `/tmp/janus-step-ca-bootstrap/step/certs/root_ca.crt`
- `modules/hosts/janus/public/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/public/ssh_user_ca_key.pub` from `/tmp/janus-step-ca-bootstrap/step/certs/ssh_user_ca_key.pub`
- `modules/hosts/janus/public/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
```
+43 -64
View File
@@ -2,84 +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";
names = [ "${hostname}.john-stream.com" ipv4 ipv6 ];
in in
{ {
flake.modules.nixos.janus-ca =
{ config, 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.login-text
nixos.step-ssh-host
nixos.janus-ca
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
nixos."${username}" nixos."${username}"
nixos.docker nixos.ssh-new
nixos.login-text nixos.mysops
nixos.step-ca # Runs the step-ca server
nixos.step-client # Uses the step-ca server as a client
nixos.mtls nixos.mtls
{ ({ 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;
}; };
sops.defaultSopsFile = ./secrets.yaml;
ssh-new.certificates = {
provisioner = "admin";
host = {
enable = true;
extraPrincipals = names;
autoRenew = true;
};
user.enable = true;
};
step-ca = {
rootCertPath = ./public/root_ca.crt;
intermediateCertPath = ./public/intermediate_ca.crt;
dnsNames = names;
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 = names;
"${hostname}.john-stream.com" bootstrap = {
"192.168.1.244" enable = true;
]; after = [ "sops-nix.service" "step-ca.service" ];
wants = [ "step-ca.service" ];
provisionerPasswordFile = config.sops.secrets."janus/admin_jwk".path;
};
}; };
})
home-manager.users."${username}" = {
imports = with inputs.self.modules.homeManager; [
mysops
];
docker.enable = true;
};
}
]; ];
}; };
} }
@@ -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-----
@@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ=
@@ -1 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNpgz9xYE3K+JeQ7vtDA07iWlp0xTXB+G8MBzX6/RluXs8E6v+ahx90M093EYIOImaW6npWQ0JnFtmZaet5l9Ao= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNpgz9xYE3K+JeQ7vtDA07iWlp0xTXB+G8MBzX6/RluXs8E6v+ahx90M093EYIOImaW6npWQ0JnFtmZaet5l9Ao=
+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
@@ -33,7 +33,6 @@ in
home.packages = with pkgs; [ home.packages = with pkgs; [
selfPkgs.jsl-zsh selfPkgs.jsl-zsh
selfPkgs.my-neovim selfPkgs.my-neovim
selfPkgs.ssh-certs
# selfPkgs.step-bootstrap # selfPkgs.step-bootstrap
# selfPkgs.wg-platform # selfPkgs.wg-platform
# self'.packages.myWrappedPackage # self'.packages.myWrappedPackage
@@ -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
]); ]);
+28 -35
View File
@@ -3,11 +3,8 @@ let
username = "john"; username = "john";
hostname = "john-pc-ubuntu"; hostname = "john-pc-ubuntu";
# testTarget = "fded:fb16:653e:25da:be24:11ff:fe89:1cc3"; # soteria
# testTarget = "fded:fb16:653e:25da:be24:11ff:fea0:753f"; # test-nix
testHost = "soteria"; # which host to test build testHost = "soteria"; # which host to test build
testTarget = "test-nix"; testTarget = "test-nix";
in in
{ {
flake.modules.homeManager."${hostname}" = { config, pkgs, lib, ... }: flake.modules.homeManager."${hostname}" = { config, pkgs, lib, ... }:
@@ -20,8 +17,8 @@ in
imports = with inputs.self.modules.homeManager; [ imports = with inputs.self.modules.homeManager; [
rebuild rebuild
john john
mtls # mtls
restic # restic
docker docker
desktop desktop
step-client step-client
@@ -36,16 +33,12 @@ in
targets.genericLinux.enable = true; targets.genericLinux.enable = true;
homeManagerFlakeDir = flakeDir;
home.username = "${username}"; home.username = "${username}";
home.homeDirectory = "/home/${username}"; home.homeDirectory = "/home/${username}";
home.packages = with pkgs; [ home.packages = with pkgs; [
nil # Nix language server nil # Nix language server
selfPkgs.jsl-zsh selfPkgs.jsl-zsh
# selfPkgs.my-neovim
selfPkgs.ssh-certs
# selfPkgs.step-bootstrap
# selfPkgs.wg-platform
# self'.packages.myWrappedPackage
(inputs.self.wrappers.test-push.apply { (inputs.self.wrappers.test-push.apply {
inherit pkgs flakeDir; inherit pkgs flakeDir;
host = testHost; host = testHost;
@@ -53,22 +46,22 @@ in
}).wrapper }).wrapper
]; ];
homeManagerFlakeDir = flakeDir;
docker.enable = true; docker.enable = true;
ssh-new = {
ssh = { certificates = {
certificates.enable = true; provisioner = "admin";
knownHosts = [ user.enable = true;
"@cert-authority fded:fb16:653e:25da:be24:11ff:fea0:753f ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ="
];
matchSets = {
certs = true;
appdaemon = true;
homelab = true;
dev = true;
tailscale = true;
}; };
}; };
# ssh = {
# matchSets = {
# certs = true;
# appdaemon = true;
# homelab = true;
# dev = true;
# tailscale = true;
# };
# };
# This provides the keys at build time and will be included in the nix store # This provides the keys at build time and will be included in the nix store
sops.defaultSopsFile = ../../../keys/secrets.yaml; sops.defaultSopsFile = ../../../keys/secrets.yaml;
@@ -81,18 +74,18 @@ in
mode = "0400"; mode = "0400";
sopsFile = ./secrets.yaml; sopsFile = ./secrets.yaml;
}; };
restic = { # restic = {
passwordFile = resticPasswordFile; # passwordFile = resticPasswordFile;
OnCalendar = "*:0/15"; # OnCalendar = "*:0/15";
paths = [ "${config.xdg.userDirs.documents}" "/conf" ]; # paths = [ "${config.xdg.userDirs.documents}" "/conf" ];
exclude = [ # exclude = [
"/home/*/Pictures" # "/home/*/Pictures"
"/home/*/Videos" # "/home/*/Videos"
"/home/*/go" # "/home/*/go"
"/home/*/snap" # "/home/*/snap"
"/home/john/john-nas" # "/home/john/john-nas"
]; # ];
}; # };
# mtls = { # mtls = {
# enable = true; # enable = true;
# subject = hostname; # subject = hostname;
+54 -49
View File
@@ -3,24 +3,27 @@
let let
username = "john"; username = "john";
hostname = "soteria"; hostname = "soteria";
ipv4 = "192.168.1.233";
ipv6 = "fded:fb16:653e:25da:be24:11ff:fe54:aa39";
names = [ "${hostname}.john-stream.com" ipv4 ipv6 ];
in 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.step-ssh-host
nixos.login-text nixos.login-text
nixos.docker inputs.home-manager.nixosModules.home-manager
nixos."${username}"
nixos.ssh-new
nixos.mtls nixos.mtls
nixos.janus-ca nixos.mysops
nixos.docker
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 [
@@ -35,55 +38,56 @@ 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 = names;
"${hostname}.john-stream.com" # lifetime = "12h";
# "192.168.1.142" # renew.onCalendar = "*:3/15";
"forgejo.john-stream.com" # renew.reloadUnits = [ "forgejo.service" "restic-rest-server.service" ];
"192.168.1.244" # certReaders = [ config.services.forgejo.user "restic" ];
];
lifetime = "12h";
renew.onCalendar = "*:3/15";
renew.reloadUnits = [ "forgejo.service" "restic-rest-server.service" ];
certReaders = [ config.services.forgejo.user "restic" ];
};
forgejo = {
enable = true;
root_url = "https://forgejo.john-stream.com";
https = true;
port = 443;
}; };
resticServer = { # forgejo = {
enable = true; # enable = true;
dataDir = "/mnt/restic"; # root_url = "https://forgejo.john-stream.com";
privateRepos = true; # https = true;
listenAddress = "0.0.0.0:8000"; # port = 443;
tls = { # };
certFile = config.mtls.certFile;
keyFile = config.mtls.keyFile; # resticServer = {
}; # enable = true;
}; # dataDir = "/mnt/restic";
# privateRepos = true;
# listenAddress = "0.0.0.0:8000";
# tls = {
# certFile = config.mtls.certFile;
# keyFile = config.mtls.keyFile;
# };
# };
loginText.extraServiceStatus = { loginText.extraServiceStatus = {
Docker = "docker"; Docker = "docker";
"mTLS Renewal" = "mtls-renew.timer"; "mTLS Renewal" = "mtls-renew.timer";
Forgejo = "forgejo.service"; "Forgejo" = "forgejo.service";
"Forgejo Backup" = "forgejo-dump.timer"; "Forgejo Backup" = "forgejo-dump.timer";
"Restic REST Server" = "restic-rest-server.service"; "Restic REST Server" = "restic-rest-server.service";
}; };
step-ssh-host.hostname = hostname; ssh-new.certificates = {
provisioner = "admin";
host = {
enable = true;
extraPrincipals = names;
autoRenew = true;
};
user.enable = true;
};
# This provides the secrets at install time # This provides the secrets at install time
sops.defaultSopsFile = ./secrets.yaml; sops.defaultSopsFile = ./secrets.yaml;
# programs.zsh.enable = true;
home-manager.users."${username}".imports = [ inputs.self.modules.homeManager.soteria ]; home-manager.users."${username}".imports = [ inputs.self.modules.homeManager.soteria ];
environment.systemPackages = [ environment.systemPackages = [
@@ -95,9 +99,10 @@ in
}; };
flake.modules.homeManager.soteria = { config, pkgs, lib, ... }: { flake.modules.homeManager.soteria = { config, pkgs, lib, ... }: {
imports = [ imports = with inputs.self.modules.homeManager; [
inputs.self.modules.homeManager.rebuild rebuild
inputs.self.modules.homeManager.mysops mysops
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;
@@ -108,12 +113,12 @@ in
]; ];
}; };
flake.homeConfigurations.soteria = withSystem "x86_64-linux" (ctx@{ config, inputs', ...}: # flake.homeConfigurations.soteria = withSystem "x86_64-linux" (ctx@{ config, inputs', ...}:
inputs.home-manager.lib.homeManagerConfiguration { # inputs.home-manager.lib.homeManagerConfiguration {
pkgs = inputs'.nixpkgs.legacyPackages; # pkgs = inputs'.nixpkgs.legacyPackages;
modules = [ # modules = [
inputs.self.modules.homeManager."${username}" # inputs.self.modules.homeManager."${username}"
inputs.self.modules.homeManager.soteria # inputs.self.modules.homeManager.soteria
]; # ];
}); # });
} }
+21 -23
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]
@@ -11,33 +9,33 @@ forgejo:
restic_password: ENC[AES256_GCM,data:u7QOZXJkxVG4J75K5nphb2uJGdz6jbWuVSsKKu+41fshp7cVoRijtr/Cs02LjVse,iv:bt1W2FeBTG6ypBFYzMPXPIkYTSn0uHURY2ui6MRgYY8=,tag:DObAMws/zQcM+UKUe9EECA==,type:str] restic_password: ENC[AES256_GCM,data:u7QOZXJkxVG4J75K5nphb2uJGdz6jbWuVSsKKu+41fshp7cVoRijtr/Cs02LjVse,iv:bt1W2FeBTG6ypBFYzMPXPIkYTSn0uHURY2ui6MRgYY8=,tag:DObAMws/zQcM+UKUe9EECA==,type:str]
sops: sops:
age: age:
- recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt - enc: |
enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5QThHOFdVQ0F6ZDE3QXdL YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLM3NML2ozWExSTlY3ZXJH
QVF0WW5yRkNtNXIyRm1LeEhwY2dRRWo0WENZCmZoWmlXZDh4ZGlUR2JYOUxuaVAx ZEpaN1RIOWVjNXRRQTlLQVZKTUUvZnBxREd3Cmp6b29aRDlUdDFwMVhwS2thODNO
WjhXVHBMNWdrdkgvTENJR2RpWldkNTAKLS0tIEF1bS9JSnFTbytBa1U5RjVzWkd3 QzNWSVIrak1uMGRuUlBONkR2aW1nbXMKLS0tIDh2YmFaRXBvZ0Q2blhjTVphL3J3
S0pKVVI2RFN5a1BMYXBEY3VOUUM5QTAKNarYZm9DKRQhosSJBn9yryFxDkmFTV/o TkROaHdwWWN4YnkyczZLbDJYRTNrQTQKvHSXjP9EZkq9DrPZZHTIlFrPUhez6jZT
i8b/tZ1ZybjEXX+X1EsgylM5u8iKkbEyUzqKO0E0gpg4qXSsJaMMYQ== LJvkmQgKluer7+lWr8XuuqtQsvhtef1di82SBG6C6YK0zWIxrMjFkA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1h0prahyukq4l564yqwgcpg3g6gdrjflk0suklussjjrjstxd9uesws8633 recipient: age1p4gyh5260ewp7t2ctzv5ewj4a06szl389fm4gzy6vltxhjj73ers87u33g
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhdEN2M1lkMFoyQzg3bCsz YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwWThXZGtFSjFZVU1UWG9v
cVB0NUlkV1JPRjQ1TW5RSVFHUjdNMERoRXlzCk9tc25iMG00TkZjWjk2S2cyVE1J Wk9sVEVWemtVWTJ6TGlOd3ZhVUZBYnJ1OFFRClpFNmEwTG1PR0pQUFJuTTVVZGRD
cUFITDh2SkZGN0lpeWJVcWM3V3JrTTQKLS0tIENOWVBzTFYrdHRsMERwV0RKS01j bnpPdjZGVUJvS0NyWGJQbEV4cUlHZncKLS0tIDFNU1VxNjZYSks1OTMrblJPbDI1
QVI5TVl4L0dwOWpoQ1I2THZ6cWovWGMKIo1x1ZbdTyr/dNlPhvuomfk2MoPLsHyU bHIrUnhsMWNhWVpVc3hCazlKTlV5YncKgR/AbgCE9JU8U8PH3ouYTZJOqL7rRODr
N3CP3Scu3aZ7vqVua7uwtv4xQqyQI/yOnFjwxrVYPJ+N5Y/b4wsC6A== 5D2zT9pcTC3LQGZrhNtgjAi6IucU4yDAU7UhT3D7cZawwFTKVaoplw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0 recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt
enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGSGQvL2ZYM2dtV1JWaDR1 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3K054SHE0V1RCYlRlTXJT
RjJnN0YwWFM4d1IwTElqUnZQQkVDTmdydUJjCjdCUWxCVDNXUHVLd2ZHcDA5L0Mw bXo4ZnE1cFozY3R2dmg5S1BacC9xb2oweWpBClJJVnkrSytFaG8xR1l4SU5FNGxi
cEVxVE84L2xmQnRoYmsxVEdMSEZjdGcKLS0tIHViRVJyTWt4Q3JrMWtYaS9QdHlS UndnNzYwMUlXSjRYK3c3MzI4NmRVRVEKLS0tIFQ4NFdPeGI4LytWK0JPcUV4VjM2
Yjd0MUcxcExvWVpCOUR3MkdZdGQyWUkKnru0Y2A98+0Mps7EtVK7ct3vPqIGveUt b1dtYWdFTTFQcW5PQml2OWs1Z0VZOFUKrDX7y3cU8KPRLKlO/HI/jdynqwO1TAS0
E5fzpcKvdefzObrx7BPTwJ19t2fZg/dSi7HKwx3vmKZSzyQaqJOzsg== WY4F0qFKmWwsJJjbnjdpYYP3O1/8RNeXjnUg8fGYY75sf7iAYWSe/w==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
lastmodified: "2026-04-04T23:18:43Z" lastmodified: "2026-04-04T23:18:43Z"
mac: ENC[AES256_GCM,data:qBgeli5lHb4pyA8nAADBuRBAaq8VbAIsFI37OZtgnbnoHW2crxo3YC+EknaIYnZpZ48kwVhQS5lGRjI6JsWWhTH3+LVAhTmS2Qj/pZTD/JDLK6XJGXS4U9nB7m9aGYyW8gFCy9/DfoJWGsS//+ZmUikKPfd5kMZgh1zGoYCIGug=,iv:h+2fA+bO2SMCNrEslP36x3BPRaIy25cU/DNX8CYSC6A=,tag:RvVyUZ4ONRaKaqGiT31eUQ==,type:str] mac: ENC[AES256_GCM,data:qBgeli5lHb4pyA8nAADBuRBAaq8VbAIsFI37OZtgnbnoHW2crxo3YC+EknaIYnZpZ48kwVhQS5lGRjI6JsWWhTH3+LVAhTmS2Qj/pZTD/JDLK6XJGXS4U9nB7m9aGYyW8gFCy9/DfoJWGsS//+ZmUikKPfd5kMZgh1zGoYCIGug=,iv:h+2fA+bO2SMCNrEslP36x3BPRaIy25cU/DNX8CYSC6A=,tag:RvVyUZ4ONRaKaqGiT31eUQ==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
-31
View File
@@ -1,31 +0,0 @@
{ inputs, ... }:
let
username = "john";
hostname = "test-nix";
in
{
flake.nixosConfigurations."${hostname}" = inputs.nixpkgs.lib.nixosSystem {
modules = with inputs.self.modules; [
nixos.lxc
nixos.mysops
nixos.step-ssh-host
inputs.home-manager.nixosModules.home-manager
nixos."${username}"
nixos.docker
{
step-ssh-host.hostname = hostname;
sops.defaultSopsFile = ../../keys/secrets.yaml;
home-manager.users."${username}" = {
imports = with inputs.self.modules.homeManager; [
mysops
];
docker.enable = true;
ssh.matchSets = {
certs = true;
homelab = true;
};
};
}
];
};
}
+15 -3
View File
@@ -9,14 +9,18 @@
# homeImports ? [ ], # homeImports ? [ ],
# homePackages ? [ ], # homePackages ? [ ],
}: { }: {
nixos."${username}" = { config, lib, pkgs, ... }: { nixos."${username}" = { config, lib, pkgs, ... }:
let
jslZsh = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.jsl-zsh;
jslZshExe = lib.getExe' jslZsh "jsl-zsh";
in {
imports = [ imports = [
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
]; ];
environment.shells = [ environment.shells = [
"${lib.getExe pkgs.zsh}" "${lib.getExe pkgs.zsh}"
"${lib.getExe inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.jsl-zsh}" jslZshExe
]; ];
users.groups."${username}" = {}; users.groups."${username}" = {};
@@ -25,7 +29,7 @@
isNormalUser = true; isNormalUser = true;
group = username; group = username;
home = "/home/${username}"; home = "/home/${username}";
shell = pkgs.zsh; shell = jslZshExe;
extraGroups = [ "input" "networkmanager" "video" "render" ] extraGroups = [ "input" "networkmanager" "video" "render" ]
++ lib.optional isAdmin "wheel" ++ lib.optional isAdmin "wheel"
++ lib.optional config.virtualisation.docker.enable "docker" ++ lib.optional config.virtualisation.docker.enable "docker"
@@ -38,6 +42,14 @@
security.sudo-rs.enable = lib.mkIf isAdmin true; security.sudo-rs.enable = lib.mkIf isAdmin true;
home-manager.useGlobalPkgs = true; home-manager.useGlobalPkgs = true;
# DetNix warns when HM generates options.json for manuals; disable manual outputs.
home-manager.sharedModules = [
{
manual.manpages.enable = false;
manual.html.enable = false;
manual.json.enable = false;
}
];
# https://github.com/Doc-Steve/dendritic-design-with-flake-parts/wiki/Dendritic_Aspects#multi-context-aspect # https://github.com/Doc-Steve/dendritic-design-with-flake-parts/wiki/Dendritic_Aspects#multi-context-aspect
home-manager.users."${username}" = { home-manager.users."${username}" = {
imports = [ self.modules.homeManager."${username}" ]; imports = [ self.modules.homeManager."${username}" ];
+2 -2
View File
@@ -2,8 +2,8 @@
flake.modules.nixos.login-text = { config, lib, ... }: flake.modules.nixos.login-text = { config, lib, ... }:
let let
defaultServiceStatus = { defaultServiceStatus = {
SSH = "sshd.socket"; "SSH Socket" = "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 {
+31 -17
View File
@@ -1,24 +1,38 @@
{ inputs, ... }: { inputs, ... }:
{ {
flake.modules.nixos.lxc = { pkgs, lib, ...}: { flake.modules.nixos.lxc = { pkgs, lib, ...}:
imports = with inputs.self.modules.nixos; [ let
({ modulesPath, ... }: { imports = [ "${modulesPath}/virtualisation/proxmox-lxc.nix" ]; }) selfPackages = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
]; rootShellPath = lib.getExe' selfPackages.jsl-zsh-tty "jsl-zsh-tty";
nixpkgs.hostPlatform = lib.mkForce "x86_64-linux"; in
system.stateVersion = "25.11"; {
nix.settings.experimental-features = [ "nix-command" "flakes" ]; imports = with inputs.self.modules.nixos; [
environment.systemPackages = with pkgs; [ git zsh ]; ({ modulesPath, ... }: { imports = [ "${modulesPath}/virtualisation/proxmox-lxc.nix" ]; })
];
nixpkgs.hostPlatform = lib.mkForce "x86_64-linux";
system.stateVersion = "25.11";
time.timeZone = "US/Central";
nix.settings.experimental-features = [ "nix-command" "flakes" ];
environment.systemPackages = with pkgs; [
git
selfPackages.jsl-zsh-tty
];
environment.shells = lib.mkAfter [ rootShellPath ];
users.users.root.shell = lib.mkForce rootShellPath;
# security.sudo-rs.enable = true; networking.nameservers = [ "192.168.1.150" ];
programs.nix-ld.enable = true; networking.dhcpcd.extraConfig = "nohook resolv.conf";
nix.optimise.automatic = true;
nix.gc = { # security.sudo-rs.enable = true;
automatic = true; programs.nix-ld.enable = true;
dates = "weekly"; nix.optimise.automatic = true;
options = "--delete-older-than 30d"; nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
programs.bash.enable = true;
}; };
programs.bash.enable = true;
};
# Generic bootstrapping lxc, use a specific host file for more # Generic bootstrapping lxc, use a specific host file for more
flake.nixosConfigurations.lxc = inputs.nixpkgs.lib.nixosSystem { flake.nixosConfigurations.lxc = inputs.nixpkgs.lib.nixosSystem {
+1 -1
View File
@@ -39,7 +39,7 @@
enableZshIntegration = true; enableZshIntegration = true;
package = ghosttyX11; package = ghosttyX11;
settings = { settings = {
command = "TERM=xterm-256color ${lib.getExe pkgs.zsh}"; command = "TERM=xterm-256color ${lib.getExe inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.jsl-zsh}";
font-size = 12; font-size = 12;
font-family = "Source Code Pro"; font-family = "Source Code Pro";
theme = "Catppuccin Mocha"; theme = "Catppuccin Mocha";
+4
View File
@@ -16,5 +16,9 @@
[[ssh-keys]] [[ssh-keys]]
vault = "Private" vault = "Private"
''; '';
programs.ssh.settings."*" = {
IdentityAgent = "${config.home.homeDirectory}/.1password/agent.sock";
};
}; };
} }
+3 -2
View File
@@ -35,9 +35,10 @@ in
config = config =
let let
identityFile = "${config.home.homeDirectory}/.ssh/${config.ssh-new.user.keyFile}";
my-sops = (inputs.self.wrappers.mySops.apply { my-sops = (inputs.self.wrappers.mySops.apply {
inherit pkgs; inherit pkgs;
sshKey = config.ssh.identityFile; sshKey = identityFile;
}).wrapper; }).wrapper;
in in
{ {
@@ -46,7 +47,7 @@ in
sops = { sops = {
defaultSopsFile = ../../keys/secrets.yaml; defaultSopsFile = ../../keys/secrets.yaml;
defaultSopsFormat = "yaml"; defaultSopsFormat = "yaml";
age.sshKeyPaths = [ "${config.ssh.identityFile}" ]; age.sshKeyPaths = [ identityFile ];
}; };
home.packages = with pkgs; [ home.packages = with pkgs; [
+60
View File
@@ -50,5 +50,65 @@
''; '';
}; };
}).wrapper; }).wrapper;
packages.starship-ascii = (inputs.wrappers.wrapperModules.starship.apply {
inherit pkgs;
settings = {
add_newline = false;
format = "$username$hostname$directory$git_branch$git_status$cmd_duration$line_break$character";
username = {
show_always = true;
format = "[$user]($style)";
style_user = "bold blue";
style_root = "bold red";
};
hostname = {
disabled = false;
ssh_only = false;
format = "[@$hostname]($style) ";
style = "bold blue";
};
directory = {
truncation_length = 3;
truncate_to_repo = true;
format = "[ in $path]($style) ";
style = "bold cyan";
};
git_branch = {
symbol = "on ";
format = "[$symbol$branch]($style) ";
style = "bold yellow";
};
git_status = {
format = "([$all_status$ahead_behind]($style) )";
style = "bold red";
ahead = "ahead:$count ";
behind = "behind:$count ";
diverged = "diverged:$ahead_count/$behind_count ";
up_to_date = "";
};
cmd_duration = {
min_time = 2000;
show_notifications = false;
format = "took [$duration]($style) ";
style = "bold green";
};
line_break.disabled = false;
character = {
success_symbol = "[\\$](bold green) ";
error_symbol = "[\\$](bold red) ";
vicmd_symbol = "[\\$](bold yellow) ";
};
};
}).wrapper;
}; };
} }
+99 -110
View File
@@ -23,127 +23,116 @@ let
in in
{ {
flake.modules = { flake.modules = {
nixos.zsh = { pkgs, ... }: { nixos.zsh = { pkgs, ... }:
users.users."${username}".shell = pkgs.zsh; let
programs.zsh.enable = true; selfPackages = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
# Already being imported by the john.nix module in
# home-manager.sharedModules = [ {
# inputs.self.modules.homeManager.zsh users.users."${username}".shell = selfPackages.jsl-zsh;
# ]; programs.zsh.enable = true;
}; };
homeManager.zsh = { pkgs, config, ... }: { homeManager.zsh = { pkgs, config, ... }:
programs.zsh = { let
enable = true; selfPackages = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
package = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.jsl-zsh; in
enableCompletion = true; {
autosuggestion.enable = true; programs.zsh = {
# syntaxHighlighting.enable = true;
initContent = ''
HOST=$(hostname -s)
${homeEndKeyBindings}
'';
dotDir = "${config.xdg.configHome}/zsh";
history = {
append = true;
ignoreAllDups = true;
ignorePatterns = [
"history"
"ls"
"eza"
"clear"
];
save = historySize;
size = historySize;
share = true;
};
oh-my-zsh = {
enable = true; enable = true;
# theme = "risto"; package = selfPackages.jsl-zsh;
theme = "agnoster"; dotDir = "${config.xdg.configHome}/zsh";
plugins = [
"sudo"
"dotenv"
"git"
"ssh"
"ssh-agent"
];
}; };
}; };
};
}; };
perSystem = { config, self', pkgs, lib, ... }: { perSystem = { config, self', pkgs, lib, ... }:
packages.jsl-zsh = let
let ignorePatterns = [
ignorePatterns = [ "ls" "eza" "history" "clear"
"ls" "eza" "history" "clear" ];
]; integrationPackages = with pkgs; [
aliasStr = lib.concatStringsSep "\n" ( fzf
lib.mapAttrsToList (k: v: "alias -- ${lib.escapeShellArg k}=${lib.escapeShellArg v}") { zoxide
ls = "eza"; ];
ll = "eza -l"; extraToolPackages = with pkgs; [
la = "eza -a"; lazygit
lt = "eza --tree"; lazydocker
lla = "eza -la"; devenv
ds = "gdu -i /snap /"; self'.packages.shell-tools
ld = "lazydocker"; self'.packages.neovim-min
}); ];
in aliasStr = lib.concatStringsSep "\n" (
(inputs.wrappers.wrapperModules.zsh.apply { lib.mapAttrsToList (k: v: "alias -- ${lib.escapeShellArg k}=${lib.escapeShellArg v}") {
inherit pkgs; ls = "eza";
binName = "jsl-zsh"; ll = "eza -l";
env = { la = "eza -a";
LANG = "en_US.UTF-8"; lt = "eza --tree";
COLORTERM = "truecolor"; lla = "eza -la";
DEVENV_SHELL_TYPE = "zsh"; ds = "gdu -i /snap /";
}; ld = "lazydocker";
settings = { });
completion = {
enable = true; mkJslZsh = { binName, starshipPackage }:
extraCompletions = true; let
caseInsensitive = true; loginBootstrapPath = lib.makeBinPath (integrationPackages ++ [ starshipPackage ] ++ extraToolPackages);
fuzzySearch = true; in
(inputs.wrappers.wrapperModules.zsh.apply {
inherit pkgs binName;
env = {
LANG = "en_US.UTF-8";
COLORTERM = "truecolor";
DEVENV_SHELL_TYPE = "zsh";
}; };
autoSuggestions = { settings = {
enable = true; completion = {
strategy = [ "history" "completion" ];
};
history = {
append = true;
expanded = true;
share = true;
ignoreAllDups = true;
ignoreSpace = true;
};
integrations = {
fzf.enable = true;
starship = {
enable = true; enable = true;
package = self'.packages.starship; extraCompletions = true;
caseInsensitive = true;
fuzzySearch = true;
};
autoSuggestions = {
enable = true;
strategy = [ "history" "completion" ];
};
history = {
append = true;
expanded = true;
share = true;
ignoreAllDups = true;
ignoreSpace = true;
}; };
zoxide.enable = true;
}; };
}; extraRC = ''
extraRC = '' ${homeEndKeyBindings}
${homeEndKeyBindings}
HISTFILE=$HOME/.config/zsh/.zsh_history # Login shells may reset PATH before integrations run.
SAVEHIST=${toString historySize} export PATH=${lib.escapeShellArg loginBootstrapPath}:$PATH
HISTORY_IGNORE=${lib.escapeShellArg "(${lib.concatStringsSep "|" ignorePatterns})"}
HOSTNAME=$(hostname -s) source <(fzf --zsh)
${aliasStr} eval "$(zoxide init zsh)"
eval "$(starship init zsh)"
eval "$(devenv hook zsh)" HISTFILE=$HOME/.config/zsh/.zsh_history
''; SAVEHIST=${toString historySize}
extraPackages = with pkgs; [ HISTORY_IGNORE=${lib.escapeShellArg "(${lib.concatStringsSep "|" ignorePatterns})"}
lazygit
lazydocker HOSTNAME=$(hostname -s)
devenv ${aliasStr}
self'.packages.shell-tools
self'.packages.neovim-min eval "$(devenv hook zsh)"
]; '';
}).wrapper; extraPackages = extraToolPackages;
}; }).wrapper;
in
{
packages.jsl-zsh = mkJslZsh {
binName = "jsl-zsh";
starshipPackage = self'.packages.starship;
};
packages.jsl-zsh-tty = mkJslZsh {
binName = "jsl-zsh-tty";
starshipPackage = self'.packages.starship-ascii;
};
};
} }
-219
View File
@@ -1,219 +0,0 @@
{ inputs, ... }:
let
userName = "john";
sshHostCAPubKey = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNug18oLH0vZxnibXJzMJvTWFPZTnSlhCDDVi+rHhgnIum6ZXQ4SF+VHOOAM5BbzZmMKitNJ5lcrGP15Eur7DzQ=";
in
{
flake.modules.nixos.ssh = { config, pkgs, lib, ... }:
let
cfg = config.ssh;
configDir = "/etc/ssh";
in
{
options.ssh = {
hostKey = lib.mkOption {
description = "String path to the host private key file";
type = lib.types.str;
default = "ssh_host_ed25519_key";
};
certificates = {
enable = lib.mkEnableOption "Enable SSH host certificates";
userCA = lib.mkOption {
description = "Content for the SSH user CA file (public key)";
type = lib.types.path;
default = ../hosts/janus/ssh_user_ca.pub;
};
userCAFile = lib.mkOption {
description = "String path to the SSh user CA";
type = lib.types.str;
default = "ssh_user_ca.pub";
};
};
};
config = {
services.openssh = {
enable = true;
# require public key authentication for better security
settings = lib.mkMerge [
{
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
HostKey = "${configDir}/${cfg.hostKey}";
}
(lib.mkIf cfg.certificates.enable {
TrustedUserCAKeys = "${configDir}/${cfg.certificates.userCAFile}";
HostCertificate = "${configDir}/${cfg.hostKey}-cert.pub";
})
];
};
environment.etc."ssh/${cfg.certificates.userCAFile}" = lib.mkIf cfg.certificates.enable {
source = cfg.certificates.userCA;
};
programs.ssh.knownHosts = lib.mkIf cfg.certificates.enable {
"192.168.1.*" = {
certAuthority = true;
publicKey = sshHostCAPubKey;
};
"*.john-stream.com" = {
certAuthority = true;
publicKey = sshHostCAPubKey;
};
};
};
};
flake.modules.homeManager.ssh = { config, pkgs, lib, ... }:
let
cfg = config.ssh;
configDir = "${config.home.homeDirectory}/.ssh";
identityFile = cfg.identityFile;
publicKeyFile = "${identityFile}.pub";
certificateFile = "${identityFile}-cert.pub";
in
{
options.ssh = with lib; {
identityFile = mkOption {
# Intentionally not using a path type here because that will end up with the private key getting copied into the store
type = types.str;
default = "${config.home.homeDirectory}/.ssh/id_ed25519";
description = "Path to the SSH identity file.";
};
certificates = {
enable = mkEnableOption "Enable SSH client certificates";
};
knownHostsFile = mkOption {
type = types.str;
default = "${configDir}/known_hosts";
};
knownHosts = mkOption {
description = "";
type = types.listOf types.str;
default = [ ];
};
matchSets = {
appdaemon = mkEnableOption "Enable AppDaemon SSH targets";
certs = mkEnableOption "Enable Janus and Soteria SSH targets";
homelab = mkEnableOption "Enable various Homelab targets";
dev = mkEnableOption "Enable development targets";
tailscale = mkEnableOption "Enable tailscale targets";
};
};
# All this stuff has to be wrapped in a config attribute because of the presence of the options here?
config = let
provisionerPasswordPath = config.sops.secrets."janus/admin_jwk".path;
in {
home.file.".ssh/known_hosts" = {
text = lib.concatStringsSep "\n" (
cfg.knownHosts ++ lib.optionals cfg.certificates.enable [
"@cert-authority 192.168.1.* ${sshHostCAPubKey}"
"@cert-authority *.john-stream.com ${sshHostCAPubKey}"
]
);
};
programs.ssh = {
enable = true;
enableDefaultConfig = false;
extraConfig = ''
SetEnv TERM="xterm-256color"
'';
settings = lib.mkMerge [
{
"john-pc-ubuntu" = {
HostName = "192.168.1.85";
};
"*" = lib.mkMerge [
{
User = "john";
IdentityAgent = "${config.home.homeDirectory}/.1password/agent.sock";
Compression = false;
ServerAliveInterval = 0;
ServerAliveCountMax = 3;
IdentitiesOnly = true;
IdentityFile = identityFile;
HashKnownHosts = false;
UserKnownHostsFile = cfg.knownHostsFile;
AddKeysToAgent = "yes";
ForwardAgent = false;
}
(lib.mkIf cfg.certificates.enable { CertificateFile = certificateFile; })
];
}
(lib.mkIf cfg.matchSets.appdaemon {
"appdaemon" = {
HostName = "192.168.1.242";
User = "appdaemon";
};
"ad-nix" = {
HostName = "192.168.1.201";
User = "appdaemon";
};
})
(lib.mkIf cfg.matchSets.certs {
"janus" = {
HostName = "janus.john-stream.com";
User = "root";
};
"soteria" = {
HostName = "soteria.john-stream.com";
User = "john";
};
})
(lib.mkIf cfg.matchSets.homelab {
"docs" = {
HostName = "192.168.1.110";
User = "root";
RequestTTY = "force";
RemoteCommand = "~/.nix-profile/bin/jsl-zsh";
};
"gitea" = {
HostName = "192.168.1.104";
User = "john";
};
"hermes" = {
HostName = "192.168.1.150";
User = "root";
# Enabling this breaks the ability of Zed to install its remote stuff
# RequestTTY = "force";
# RemoteCommand = "/root/.nix-profile/bin/jsl-zsh";
};
"panoptes" = {
HostName = "192.168.1.107";
User = "panoptes";
};
})
(lib.mkIf cfg.matchSets.dev {
"test-nix" = {
HostName = "fded:fb16:653e:25da:be24:11ff:fea0:753f";
User = "john";
RequestTTY = "auto";
# RemoteCommand = "/run/current-system/sw/bin/jsl-zsh";
};
})
(lib.mkIf cfg.matchSets.tailscale {
"jdl-docker" = {
HostName = "jdl-docker.tailcf205.ts.net";
User = "john";
RequestTTY = "auto";
# RemoteCommand = "~/.nix-profile/bin/jsl-zsh";
};
})
];
};
};
};
}
-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"
}
+46 -49
View File
@@ -1,94 +1,91 @@
{ 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; wrappers = inputs.self.wrappers;
sshKeyPath = "/etc/ssh/ssh_host_ed25519_key"; sshKeyPath = "/etc/ssh/ssh_host_ed25519_key";
sshCertPath = "${sshKeyPath}-cert.pub"; sshCertPath = "${sshKeyPath}-cert.pub";
sshHostCertSign = (wrappers.signHostWrapper.apply {
inherit pkgs;
inherit (cfg) provisioner extraPrincipals;
provisionerPasswordFile = config.sops.secrets."janus/admin_jwk".path;
}).wrapper;
sshHostCertRenew = (wrappers.renewHostWrapper.apply {
inherit pkgs;
sshHostKeyFile = sshKeyPath;
overwrite = true;
}).wrapper;
sshHostCertCheck = (wrappers.hostCheckWrapper.apply {
inherit pkgs;
certPath = sshCertPath;
}).wrapper;
sshHostRenewalCheck = (wrappers.renewalCheck.apply {
inherit pkgs;
certPath = sshCertPath;
expires-in = "4h";
}).wrapper;
in in
{ {
# NixOS Options # NixOS Options
options.step-ssh-host = { options.ssh-certs = {
hostname = lib.mkOption {
description = "Networking host name to register with the CA";
type = lib.types.str;
};
provisioner = lib.mkOption { provisioner = lib.mkOption {
description = "Provisioner inside Step CA to use for the SSH certificates"; description = "Provisioner inside Step CA to use for the SSH certificates";
type = lib.types.str; type = lib.types.str;
default = "admin"; default = "admin";
}; };
extraPrincipals = lib.mkOption {
description = "Additional SSH host certificate principals to include per host";
type = with lib.types; listOf str;
default = [ ];
};
}; };
imports = with inputs.self.modules.nixos; [ ssh ]; # imports = with inputs.self.modules.nixos; [ ssh ];
# NixOS Config # NixOS Config
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 sshHostCertSign
(writeShellScriptBin "ssh-host-cert-renew" '' sshHostCertRenew
${lib.getExe pkgs.step-cli} ssh certificate \ sshHostRenewalCheck
--host --sign \ sshHostCertCheck
--provisioner "${cfg.provisioner}" \
--provisioner-password-file "${provisionerPasswordPath}" \
--principal "${cfg.hostname}" \
--principal "${cfg.hostname}.john-stream.com" \
"${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 = "SSH host certificate renewal";
wantedBy = [ ]; wantedBy = [ "multi-user.target" ];
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; [ step-cli systemd ];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
User = "root"; User = "root";
Group = "root"; Group = "root";
ExecCondition = lib.getExe sshHostRenewalCheck;
ExecStart = lib.getExe sshHostCertRenew;
}; };
script = ''
set -euo pipefail
if ${lib.getExe pkgs.step-cli} ssh needs-renewal "${sshCertPath}" --expires-in "4h"; then
echo "Renewing SSH host certificate"
else
rc=$?
if [ "$rc" -eq 1 ]; then
echo "SSH host cert does not need renewal"
exit 0
fi
if [ "$rc" -eq 2 ]; then
echo "SSH host cert missing: ${sshCertPath}" >&2
exit 1
fi
echo "step ssh needs-renewal failed with rc=$rc" >&2
exit "$rc"
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" ];
timerConfig = { timerConfig = {
OnBootSec = "5m"; OnBootSec = "5m";
OnUnitActiveSec = "4h"; OnUnitActiveSec = "4h";
RandomizedDelaySec = "15m"; RandomizedDelaySec = "15m";
Persistent = true; Persistent = true;
Unit = "step-ssh-host-renew.service"; Unit = "ssh-certs-renew.service";
}; };
}; };
}; };
+183 -18
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, ... }:
# https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/security/step-ca.nix let
services.step-ca = { cfg = config.step-ca;
enable = true; caAddress = "0.0.0.0";
openFirewall = true; caPort = 443;
address = ipAddress;
port = 8443;
# https://smallstep.com/docs/step-ca/configuration/#configuration-options caPasswordPath = (lib.getAttr cfg.secrets.caPassword config.sops.secrets).path;
settings = { intermediateKeyPath = (lib.getAttr cfg.secrets.intermediateKey config.sops.secrets).path;
root = ""; sshHostCaKeyPath = (lib.getAttr cfg.secrets.sshHostCaKey config.sops.secrets).path;
crt = ""; 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;
}; };
}; };
environment.systemPackages = with pkgs; [ in
step-ca {
step-cli 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
services.step-ca = {
enable = true;
openFirewall = true;
address = caAddress;
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 ];
environment.systemPackages = with pkgs; [
step-ca
step-cli
];
};
}; };
} }
+31 -19
View File
@@ -1,4 +1,4 @@
{ self, inputs, lib, ... }: { self, inputs, ... }:
let let
username = "john"; username = "john";
baseUserModules = self.factory.user { baseUserModules = self.factory.user {
@@ -14,36 +14,48 @@ in
key = ""; key = "";
keygrip = [ ]; keygrip = [ ];
authorizedKeys = [ authorizedKeys = [
# "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIAUa4dcg1TWc4pW++uodyhX4eOqrX/QYIxFWtEP7HFJ john@john-pc-ubuntu" # Shared keys for every host can go here.
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMOkGLo4N/L3RYvaIZ1FmePlxa1HK0fMciZxKtRhN58F root@janus" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIAUa4dcg1TWc4pW++uodyhX4eOqrX/QYIxFWtEP7HFJ john@john-pc-ubuntu"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFn5ilhqaeDsOWSk7y29se2NvxGm8djlfL3RGLokj0q6 john@john-p14s"
]; ];
}; };
flake.modules = { flake.modules = {
nixos."${username}" = { ... }: { nixos."${username}" = { config, pkgs, ... }: let
selfMeta = inputs.self.meta.users."${username}";
in {
imports = [ imports = [
baseUserModules.nixos."${username}" baseUserModules.nixos."${username}"
]; ];
users.users."${username}" = { users.users."${username}" = {
openssh.authorizedKeys.keys = inputs.self.meta.users."${username}".authorizedKeys; openssh.authorizedKeys.keys = selfMeta.authorizedKeys;
}; };
}; };
# This module will be imported by the user factory # This module will be imported by the user factory
homeManager."${username}" = { pkgs, ... }: homeManager."${username}" = { pkgs, ... }: let
with inputs.self.meta.users."${username}"; { selfMeta = inputs.self.meta.users."${username}";
home.stateVersion = "25.11"; selfPkgs = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
imports = [ in {
inputs.self.modules.homeManager.shell-tools home.stateVersion = "25.11";
inputs.self.modules.homeManager.ssh imports = with inputs.self.modules.homeManager; [
inputs.self.modules.homeManager.git ssh-new
]; shell-tools
# home.packages = [ git
# inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.shell-tools ];
# ]; xdg.enable = true;
xdg.enable = true; home.packages = [
programs.git.settings.user.name = name; selfPkgs.neovim-min
programs.git.settings.user.email = email; ];
home.sessionVariables = {
EDITOR = "nvim";
VISUAL = "nvim";
GIT_EDITOR = "nvim";
SOPS_EDITOR = "nvim";
}; };
programs.git.settings.user.name = selfMeta.name;
programs.git.settings.user.email = selfMeta.email;
programs.git.settings.core.editor = "nvim";
};
}; };
} }