60 Commits
Author SHA1 Message Date
John Lancaster 809b742821 forgejo service tweaks 2026-07-19 10:59:17 -05:00
John Lancaster 1d9566115c flake.lock updates 2026-07-18 13:52:09 -05:00
John Lancaster ac5128bf74 forgejo deployment 2026-07-18 12:21:03 -05:00
John Lancaster 25ecb7ad06 forgejo reactivate 2026-07-10 00:04:34 -05:00
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
John Lancaster 4e5f1230ab host table 2026-07-01 22:40:04 -05:00
John Lancaster c3eeeb16f8 flake updates 2026-07-01 22:39:57 -05:00
John Lancaster 7556b13eb9 sublime fix 2026-07-01 22:35:55 -05:00
John Lancaster a72f34ab51 fix 2026-07-01 14:52:40 -05:00
John Lancaster 4e465b1846 allowing broken sublime 2026-07-01 14:35:58 -05:00
John Lancaster d2737a42e8 fix 2026-07-01 14:35:37 -05:00
John Lancaster a9ae07d5c1 zsh notes 2026-07-01 00:53:17 -05:00
John Lancaster f505100bb4 step ssh-host notes 2026-07-01 00:31:13 -05:00
John Lancaster aff22e3a0f soteria notes updates 2026-07-01 00:25:50 -05:00
John Lancaster b13b774738 consolidated options 2026-07-01 00:19:17 -05:00
John Lancaster 43fe37d4e2 soteria host notes 2026-07-01 00:03:48 -05:00
John Lancaster a98e637ce2 rename 2026-06-30 23:34:27 -05:00
John Lancaster 0e194e5d44 naming conventions 2026-06-30 23:31:11 -05:00
John Lancaster 68737a513f instructions files 2026-06-30 23:30:25 -05:00
John Lancaster e76fc00a98 hosts instructions 2026-06-30 23:08:46 -05:00
59 changed files with 2688 additions and 1877 deletions
@@ -0,0 +1,68 @@
# Host Naming Discrepancy Report
This report compares the current `modules/hosts` tree against the naming rules for new hosts in `hosts.instructions.md`.
## Rules Checked
- The host directory slug should be the canonical host identity.
- A directory-backed host should prefer `default.nix` as its main entrypoint.
- The canonical slug should line up with the primary `hostname` binding, `flake.nixosConfigurations.<slug>`, and `networking.hostName`.
- A host-local Home Manager module should use the same slug.
- A standalone Home Manager configuration should prefer the key `"<username>@<slug>"`.
- Host-local helper module names should be prefixed by the host slug.
## Summary
- Recently normalized: `john-kde`, `omen-nixos`.
- Hosts that fit the new rules closely: `janus`, `john-kde`, `omen-nixos`, `test-nix`.
- Hosts with notable discrepancies: `john-p14s`, `soteria`.
- Approved exception: `john-pc` directory uses immutable deployed slug `john-pc-ubuntu` for exported keys.
## Detailed Discrepancies
### `john-p14s`
- The main host export uses the canonical slug: `flake.nixosConfigurations.john-p14s`.
- The host-local helper modules are named `p14sConfiguration` and `p14sHardware`, which drop the `john-` prefix and therefore do not use the full host slug.
- `networking.hostname` uses the correct host value, but the option name differs from the dominant `networking.hostName` spelling used elsewhere in this tree.
### `john-pc`
- Approved exception for immutable deployed hostname:
- Directory slug is `john-pc`, but exported keys and hostname bindings intentionally use `john-pc-ubuntu`.
- `hostname` binding, `flake.modules.homeManager.<name>`, and `flake.homeConfigurations` are aligned to `john-pc-ubuntu`.
- Shared SSH alias intentionally remains `john-pc-ubuntu` in `modules/services/ssh.nix`.
### `omen-nixos`
- No active discrepancy after normalization:
- The directory slug, `flake.nixosConfigurations` key, host-local module keys, and `networking.hostName` are now aligned to `omen-nixos`.
### `john-kde`
- No active discrepancy after normalization:
- The directory slug, `hostname` binding, host-local Home Manager module key, and standalone Home Manager key are now aligned to `john-kde`.
### `soteria`
- Naming is mostly coherent across directory slug, `hostname`, `flake.nixosConfigurations.soteria`, and `flake.modules.homeManager.soteria`.
- The standalone Home Manager export is `flake.homeConfigurations.soteria`, which does not follow the preferred `"<username>@<slug>"` form.
- The main host entrypoint is `soteria.nix` instead of the preferred `default.nix`.
## No Discrepancy Found
### `janus`
- Directory slug, `hostname`, `flake.nixosConfigurations.janus`, and `networking.hostName` are aligned.
- The host-local helper module `janus-ca` is clearly prefixed by the host slug.
### `test-nix`
- The single-file host uses the same slug for the file stem, `hostname`, and `flake.nixosConfigurations.test-nix`.
- It does not define `networking.hostName`, but it does not contradict the slug anywhere else.
## Follow-up Candidates
- Rename `john-p14s` helper module keys to include the full host slug (`john-p14s-*`) if strict slug consistency is desired.
- Decide whether `soteria` should move to `default.nix` and whether its standalone Home Manager key should include `john@`.
- If `john-pc-ubuntu` ever becomes changeable, decide whether to rename the directory to match or keep this as a permanent exception.
@@ -0,0 +1,78 @@
---
description: "Use when defining or modifying hosts and host-local data under modules/hosts. Explains how this repo exports nixosConfigurations and homeConfigurations, when to create flake.modules.nixos or flake.modules.homeManager helpers, and how secrets, defaults, keys, and hardware files fit into a host definition."
applyTo: 'modules/hosts/**/*.nix, modules/hosts/**/secrets.yaml, modules/hosts/**/defaults.json, modules/hosts/**/fingerprint, modules/hosts/**/*.pub'
---
# Host Definitions
Host files under `modules/hosts` are flake entrypoints, not just loose Nix snippets. Because `modules` is auto-imported through `import-tree` and `flake-file`, files here usually export one or more of these attributes:
- `flake.nixosConfigurations.<name>` for a bootable NixOS machine or container.
- `flake.homeConfigurations.<name>` for a standalone Home Manager target.
- `flake.modules.nixos.<name>` or `flake.modules.homeManager.<name>` when a host is split into reusable host-local modules that are then assembled by a nearby entrypoint.
Follow the existing host patterns in this tree:
- Keep the host entrypoint file focused on composing modules into `nixosSystem` or `homeManagerConfiguration`.
- If a host has substantial machine-specific logic, put that logic in sibling files such as `configuration.nix` or `hardware.nix`, export them as `flake.modules.nixos.<name>`, and have the entrypoint import those modules.
- For Home Manager only hosts, define `flake.modules.homeManager.<name>` and then expose a matching `flake.homeConfigurations` entry.
- Prefer composing from shared modules in `self.modules.nixos` or `inputs.self.modules.homeManager` instead of re-implementing shared behavior inline.
Treat host-local data as part of the host definition:
- Keep hardware-specific settings in the host directory, typically in `hardware.nix`.
- Keep install-time or machine-specific configuration in the same host directory, typically in `configuration.nix`.
- Keep host-local secret references beside the host and wire them through `sops.defaultSopsFile` and `mysops.hostSecretFile` instead of pointing at unrelated locations.
- Preserve nearby auxiliary files such as `defaults.json`, fingerprints, public keys, and `secrets.yaml`; these are part of the host contract.
For host-adjacent data files:
- Keep `secrets.yaml` scoped to the host that consumes it; do not reuse another host's secret file as a shortcut.
- Treat files such as `defaults.json`, `fingerprint`, and `*.pub` as inputs consumed by the host module. Update references together with the data when changing paths or filenames.
- Avoid moving or renaming these files unless the corresponding Nix references are updated in the same change.
Naming in this tree is not perfectly uniform, so preserve existing interfaces unless the task is explicitly a rename:
- The directory name, exported flake key, and `networking.hostName` may differ.
- Some hosts export from `default.nix`, while others keep the main definition in a differently named file such as `soteria.nix`.
- Do not normalize names or move files just for consistency unless the user asks for that structural change.
Naming rules for new hosts:
- Treat the host directory name as the canonical host slug for new work.
- Prefer `modules/hosts/<slug>/default.nix` as the main entrypoint for a host directory.
- Use the same slug for the primary `hostname` binding, `flake.nixosConfigurations.<slug>`, and `networking.hostName` unless the task explicitly requires a different deployed hostname.
- If the host exports a host-local Home Manager module, name it `flake.modules.homeManager.<slug>`.
- For standalone Home Manager configurations, prefer `flake.homeConfigurations."<username>@<slug>"` so the exported key still carries the host slug.
- Name host-local helper modules with the same slug as a prefix, for example `flake.modules.nixos.<slug>-hardware` or `flake.modules.nixos.<slug>-configuration`, to make ownership obvious and avoid collisions with shared modules.
- Keep host-local secrets and auxiliary files under the same host slug directory. Do not point a new host at another host's path just because the contents are similar.
Allowed exception for immutable deployed hostnames:
- If the deployed hostname is externally constrained and cannot change, use that deployed hostname as the canonical slug for exported keys and `hostname` bindings, even if the directory name differs.
- In that case, keep the exception explicit in comments or reports so future cleanup work does not accidentally rename a live hostname contract.
When working in existing hosts that predate these rules:
- Preserve the current public names by default.
- If the user asks for a rename or cleanup, update the directory slug, exported flake keys, host-local module names, and `networking.hostName` together in one change so the host identity stays coherent.
Use nearby hosts as composition examples:
- `john-p14s` splits the reusable machine logic into `configuration.nix` and `hardware.nix`, exports `flake.modules.nixos.p14sConfiguration` and `flake.modules.nixos.p14sHardware`, and assembles them from `default.nix`.
- `janus` defines a host-local reusable module (`flake.modules.nixos.janus-ca`) in the same file that also exports the final `flake.nixosConfigurations.janus` host.
- `john-pc` is a Home Manager target, so it exports a `flake.modules.homeManager` module and a `flake.homeConfigurations` entry rather than a `nixosConfiguration`.
- `soteria` combines a NixOS host, a host-local Home Manager module, and host-local secrets in one host directory.
When adding or changing a host:
- Mirror the local style first. Small hosts may define the full configuration inline; larger hosts should split reusable modules out.
- Keep `let` bindings such as `username`, `hostname`, `flakeDir`, and package aliases near the top when the file already follows that pattern.
- Add shared behavior by importing existing modules, not by copying option blocks between hosts.
- If the change affects both system and Home Manager state for a host, update both sides in the same host area when that host already models both.
For reviews and answers, distinguish between these layers:
- host entrypoint wiring in `modules/hosts/...`
- reusable shared modules in `modules/nixos`, `modules/programs`, `modules/services`, and `modules/users`
- host-local modules exported from a host directory and then consumed by its entrypoint
@@ -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,54 @@
---
description: "Use when defining or modifying reusable Nix modules outside modules/hosts. Covers flake.modules.nixos, flake.modules.homeManager, and flake.factory.user exports, and explains how shared modules differ from concrete host wiring."
applyTo: 'modules/nixos/**/*.nix, modules/users/**/*.nix, modules/features/**/*.nix, modules/programs/**/*.nix, modules/services/**/*.nix, modules/nix-tools/**/*.nix'
---
# Shared Modules
Files outside `modules/hosts` generally define reusable building blocks, not concrete machines. In this repo, those files usually export one of these surfaces:
- `flake.modules.nixos.<name>` for reusable NixOS modules.
- `flake.modules.homeManager.<name>` for reusable Home Manager modules.
- `config.flake.factory.user` for factories that generate per-user module sets.
- `flake.meta.*` when the file owns reusable metadata that other modules consume.
Keep the boundary between shared modules and hosts clear:
- Shared modules should not define `flake.nixosConfigurations.<name>` or `flake.homeConfigurations.<name>`; those belong in `modules/hosts`.
- Prefer generic options, imports, and composition over host-specific literals.
- If a setting only makes sense for one machine, keep it in that host directory instead of moving it into a shared module.
- When a host imports a shared module, treat the shared module as part of the stable interface that multiple hosts may depend on.
Follow the existing export patterns in this repo:
- Simple reusable modules may export a single module directly, such as `flake.modules.nixos.games`.
- Cross-cutting features often live under `modules/features` even when they export `flake.modules.nixos.*` or `flake.modules.homeManager.*`.
- Program and service integrations commonly export one or both module types from a single file.
- User definitions under `modules/users` may export metadata plus paired NixOS and Home Manager modules for the same user.
For user modules specifically:
- Keep reusable user facts under `flake.meta.users.<name>` when other modules need to reference them.
- Prefer deriving the NixOS side from `self.factory.user` when the file already follows that pattern.
- Keep the user-facing Home Manager module in `flake.modules.homeManager.<name>` and let the factory or host wire it into a concrete configuration.
- Put user-specific authorized keys, identity, and shared defaults here rather than duplicating them across hosts.
Design shared modules as composable interfaces:
- Import other shared modules instead of copying option blocks.
- Add options or parameters when behavior needs to vary between hosts.
- Avoid embedding host-specific paths, hostnames, addresses, or secret file locations unless the file is intentionally host-local.
- Preserve exported attribute names even when the filename is different. In this repo, the path is not always the public API name.
Use nearby files as examples:
- `modules/nixos/games.nix` is a minimal shared NixOS module with no host wiring.
- `modules/features/nixos-base.nix` defines a reusable base system module that other hosts import.
- `modules/users/john.nix` combines `flake.meta.users.john`, a reusable NixOS user module, and a reusable Home Manager user module built around the user factory.
- `modules/nix-tools/user.nix` defines the `flake.factory.user` helper that shared user modules build on.
For reviews and answers, separate these concerns clearly:
- reusable module API and option design in shared module directories
- concrete host assembly in `modules/hosts`
- whether a change increases reuse or accidentally pulls machine-specific behavior into a shared layer
@@ -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.
@@ -0,0 +1,139 @@
---
description: "Use when modifying the Soteria host definition or host-local secrets under modules/hosts/soteria. Covers host wiring and SSH cert mechanics specific to Soteria."
name: "Soteria Host Instructions"
applyTo: "modules/hosts/soteria/**/*.nix, modules/hosts/soteria/secrets.yaml"
---
# Soteria Host Instructions
Use this instruction when changing Soteria host wiring in `modules/hosts/soteria`.
## Host Intent And Shape
- Treat Soteria as a NixOS service node for Forgejo, RESTic server, mTLS, and a paired Home Manager profile for `john`.
- Keep host identity aligned: `hostname = "soteria"`, `networking.hostName = hostname`, and `flake.nixosConfigurations."${hostname}"`.
- Keep host-local secrets in `modules/hosts/soteria/secrets.yaml` and wire with `sops.defaultSopsFile = ./secrets.yaml`.
- Keep host-specific Home Manager wiring in `flake.modules.homeManager.soteria`, and keep `mysops.hostSecretFile` pointing at the Soteria secrets file.
## Required Module Composition
When editing `modules/hosts/soteria/default.nix`, preserve this composition unless the task explicitly changes architecture:
- 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.restic-server` so the RESTic REST server implementation lives in the shared module rather than inline host wiring.
- Keep `ssh-certs.hostname = hostname;` so cert principals match host naming.
## SSH Cert Mechanics On Soteria
Soteria SSH certificate behavior is the result of multiple modules. Keep this flow intact:
1. `soteria/default.nix` imports `nixos.ssh-certs` and sets `ssh-certs.hostname = "soteria"` (via `hostname`).
2. `modules/services/step-ca/ssh-host.nix`:
- Enables `ssh.certificates.enable = true`.
- 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`.
- Exposes `ssh-host-cert-renew` and `ssh-host-cert-check` helper binaries.
- Schedules `ssh-certs-renew.timer` every 4h with jitter.
3. `modules/services/ssh.nix` consumes `ssh.certificates.enable` and configures OpenSSH to:
- Set `TrustedUserCAKeys = /etc/ssh/ssh_user_ca_key.pub`.
- Set `HostCertificate = /etc/ssh/ssh_host_ed25519_key-cert.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.
Current implementation note:
- `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
Soteria secret handling spans host-local secrets, NixOS secret materialization, and Home Manager secret tooling. Keep this flow intact:
1. `modules/hosts/soteria/default.nix` imports `nixos.mysops` and sets `sops.defaultSopsFile = ./secrets.yaml`.
2. The SOPS file `modules/hosts/soteria/secrets.yaml` is the canonical encrypted source for this host's system secrets.
3. Shared modules imported by Soteria declare required secret entries under `sops.secrets` and consume them through `config.sops.secrets.<name>.path`:
- `modules/services/step-ca/ssh-host.nix` declares and consumes `janus/admin_jwk` as the Step provisioner password file.
- `modules/features/forgejo.nix` declares and consumes `forgejo/secret_key`, `forgejo/internal_token`, `forgejo/jwt_secret`, and `forgejo/lfs_jwt_secret`.
4. Secret ownership is module-defined and must stay aligned with service users:
- Forgejo secrets are owned by `config.services.forgejo.user`.
- Step SSH provisioner secret is locked to root ownership and `0400` mode.
5. `flake.modules.homeManager.soteria` imports `homeManager.mysops` and sets `mysops.hostSecretFile` to the same Soteria host secret file path. This drives helper tooling such as `edit-secrets` while Home Manager keeps its own default SOPS context from `modules/programs/sops.nix`.
Current implementation note:
- `restic_password` exists in `modules/hosts/soteria/secrets.yaml`, but it is not currently consumed by the Soteria NixOS module graph.
- Changing, deleting, or renaming seemingly unused keys in `secrets.yaml` should be treated as a compatibility change and confirmed by the task.
## Restic REST Server On Soteria
Soteria's RESTic REST server is implemented by the shared `nixos.restic-server` module and configured by host-local `resticServer` values. Trace it this way:
1. `modules/hosts/soteria/default.nix` imports `nixos.restic-server` and configures the host-local `resticServer` option set with:
- `enable = true`
- `dataDir = "/mnt/restic"`
- `privateRepos = true`
- `listenAddress = "0.0.0.0:8000"`
- `tls.certFile = config.mtls.certFile`
- `tls.keyFile = config.mtls.keyFile`
2. `modules/features/restic.nix` maps those `resticServer` options into `services.restic.server` and appends TLS flags when both TLS paths are set.
3. The shared module keeps the server base behavior in one place:
- `services.restic.server.enable = true`
- `services.restic.server.dataDir = cfg.dataDir`
- `services.restic.server.listenAddress = cfg.listenAddress`
- `services.restic.server.privateRepos = cfg.privateRepos`
- `services.restic.server.extraFlags = cfg.extraFlags ++ tlsFlags`
4. `modules/features/restic.nix` also opens the matching TCP port through `networking.firewall.allowedTCPPorts`, deriving it from `resticServer.listenAddress`.
5. The mTLS renewal hook includes `restic-rest-server.service` in `mtls.renew.reloadUnits`, so certificate rotation reloads the REST server alongside Forgejo.
6. `loginText.extraServiceStatus` exposes the running unit as `restic-rest-server.service`, which is the service name to keep in mind for status and reload behavior.
### Storage Location
- Soteria explicitly sets `resticServer.dataDir = "/mnt/restic"`.
- The shared module maps that value into `services.restic.server.dataDir`, so `/mnt/restic` is now the intended repository storage location for this host.
- The shared module also derives the firewall port from `resticServer.listenAddress`, so Soteria no longer needs a separate `networking.firewall.allowedTCPPorts = [ 8000 ]` line.
- If storage needs to move later, change the `resticServer.dataDir` input or the shared module contract, not ad hoc service cleanup logic.
- `privateRepos = true` constrains repo exposure behavior, but it does not define storage location by itself.
Current implementation note:
- The REST server is TLS-protected but currently passed `--no-auth`, so client access control relies on transport/security model rather than rest-server password auth.
- The shared module default listen address is not Soteria's deployed value; Soteria deliberately overrides it to `0.0.0.0:8000` in the host.
## Forgejo Implementation On Soteria
Soteria's Forgejo setup is split between host-local option values and the shared `nixos.forgejo` module. Trace it this way:
1. `modules/hosts/soteria/default.nix` imports `nixos.forgejo` and enables it with:
- `forgejo.enable = true`
- `forgejo.root_url = "https://forgejo.john-stream.com"`
- `forgejo.https = true`
- `forgejo.port = 443`
2. `modules/features/forgejo.nix` defines `forgejo.port` as the controlling option for the web listener. Its default is `3000`, but Soteria overrides it to `443`.
3. The shared module wires that option into multiple places:
- `services.forgejo.settings.server.HTTP_PORT = cfg.port`
- `networking.firewall.allowedTCPPorts = [ cfg.port ]` when `openFirewall = true`
- If `cfg.port < 1024`, the Forgejo systemd unit gets `CAP_NET_BIND_SERVICE` so it can bind a privileged port.
4. HTTPS on Soteria is terminated directly by Forgejo using mTLS-managed files:
- `services.forgejo.settings.server.PROTOCOL = "https"`
- `KEY_FILE = config.mtls.keyFile`
- `CERT_FILE = config.mtls.certFile`
### Storage Locations
- This repo does not override Forgejo's primary state directory in the Soteria host or in `modules/features/forgejo.nix`.
- That means Forgejo storage follows the underlying NixOS `services.forgejo` defaults, exposed in this config as `config.services.forgejo.stateDir`.
- PostgreSQL storage is also left at the underlying module default and is referenced as `config.services.postgresql.dataDir`.
- The cleanup helpers in `modules/features/forgejo.nix` confirm these are the intended storage anchors because they remove exactly `config.services.forgejo.stateDir` and `config.services.postgresql.dataDir`.
- Forgejo backup dumps are generated by `forgejo-dump.service` using `--work-path ${config.services.forgejo.dump.backupDir}`, so dump staging/output follows the Forgejo module's backup directory setting rather than a Soteria-specific path override.
Current implementation note:
- If you need to change Forgejo storage location on Soteria, do it by setting the underlying Forgejo or PostgreSQL service directory options explicitly, not by changing cleanup scripts alone.
- If you need to change the external Forgejo endpoint, update `forgejo.root_url`, `forgejo.port`, and the Soteria mTLS SAN list together.
## Change Safety Rules
- 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.
- If changing hostnames or domains, update cert principals in `ssh-certs` and corresponding SSH client targets together.
- If adding or removing `nixos.ssh-certs`, explain impact on SSH cert issuance, renewal, and trust in the change summary.
@@ -0,0 +1,54 @@
---
description: "Use when modifying modules/services/step-ca/ssh-host.nix. Covers Step SSH host certificate wiring, secret key contract, and host-side expectations required by consumers."
name: "Step SSH Host Module Instructions"
applyTo: "modules/services/step-ca/ssh-host.nix"
---
# Step SSH Host Module Instructions
Use this instruction when changing the shared NixOS module in modules/services/step-ca/ssh-host.nix.
## Module Contract
- 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:
- ssh-certs.hostname (required host identity used for cert principals)
- ssh-certs.provisioner (default: "admin")
- Keep imports = [ inputs.self.modules.nixos.ssh ] so OpenSSH host certificate settings stay composed through the shared SSH module.
## Secret Key Contract
- This module currently consumes `config.sops.secrets."janus/admin_jwk"` as the Step provisioner password file.
- Treat `janus/admin_jwk` as a public contract key name for current consumers. If you rename it, update all consumers and host secret files in the same change.
- Keep secret permissions strict (root ownership and 0400 mode).
- Do not assume a local sops file path in this module. The consuming host must define sops.defaultSopsFile.
## Host Expectations
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).
- `sops.defaultSopsFile` containing `janus.admin_jwk` (or equivalent if contract is intentionally changed everywhere).
- `ssh-certs.hostname` matching deployed host identity and DNS naming expectations.
## SSH Certificate Paths And Principals
- Preserve the default host key path contract at `/etc/ssh/ssh_host_ed25519_key` unless task scope requires coordinated changes.
- Keep host certificate path aligned to `${sshKeyPath}-cert.pub`.
- Keep principal issuance based on hostname and hostname.john-stream.com unless domain policy is intentionally migrated.
- If changing key/cert filenames or principal naming, update `modules/services/ssh.nix` integration and host rollout notes in the same change.
## Renewal Behavior
- Maintain timer/service cadence unless explicitly requested:
- timer OnUnitActiveSec = 4h
- randomized delay enabled
- Keep explicit handling for step ssh needs-renewal return codes to avoid silent failures.
- If changing renewal semantics, document whether the service now only checks state or also performs certificate issuance.
## Validation Checklist
After editing this module, validate at least one consuming host with:
1. nix eval .#nixosConfigurations.janus.config.system.build.toplevel.drvPath
2. nix build .#nixosConfigurations.janus.config.system.build.toplevel --no-link --dry-run
@@ -0,0 +1,65 @@
---
description: "Use when modifying jsl-zsh, home-manager programs.zsh settings, or shell-tools wiring. Explains when to use wrapped jsl-zsh versus pkgs.zsh and how shell-tools composes into user and host configs."
applyTo: 'modules/programs/zsh.nix, modules/features/shell-tools.nix, modules/users/**/*.nix, modules/nix-tools/user.nix, modules/services/ssh.nix, modules/hosts/**/*.nix'
---
# jsl-zsh and shell-tools wiring
This repo intentionally uses two zsh variants for different layers:
- `pkgs.zsh` is the base system shell used for user login shell declarations.
- `inputs.self.packages.<system>.jsl-zsh` is the wrapped interactive shell used by Home Manager and remote shell entrypoints.
Treat these as distinct roles, not interchangeable defaults.
## Canonical definitions
The canonical wrapped shell is defined in `modules/programs/zsh.nix`:
- `perSystem.packages.jsl-zsh` wraps zsh via `wrapperModules.zsh.apply`.
- Wrapper settings, aliases, history behavior, devenv hook, and extra package PATH come from this package definition.
- `binName = "jsl-zsh"` is part of the external contract; keep it stable unless performing an explicit repo-wide rename.
The Home Manager zsh module in the same file is the canonical consumer:
- `flake.modules.homeManager.zsh` sets `programs.zsh.package` to `self.packages.<system>.jsl-zsh`.
- Keep Home Manager interactive zsh bound to the wrapped package, not plain `pkgs.zsh`.
## shell-tools composition
`modules/features/shell-tools.nix` is a composition module, not just a package list:
- `flake.modules.homeManager.shell-tools` imports `homeManager.zsh` and `homeManager.files`.
- It adds the wrapped `shell-tools` package to `home.packages`.
- `home.shell.enableShellIntegration = true` is expected to stay with this module.
If a user module imports `homeManager.shell-tools` (for example `modules/users/john.nix`), that user already receives the Home Manager zsh wiring transitively.
## System-layer expectations
`modules/nix-tools/user.nix` keeps system login semantics separate from Home Manager interactive behavior:
- `users.users.<name>.shell = pkgs.zsh` remains the login shell declaration.
- `programs.zsh.enable = true` enables system zsh support.
- `environment.shells` includes both `${lib.getExe pkgs.zsh}` and `${lib.getExe self.packages.<system>.jsl-zsh}` so wrapped shell paths are recognized when needed.
Do not replace login shell declarations with `jsl-zsh` unless the task is explicitly changing system login policy.
## Host and SSH usage contracts
Hosts may include `selfPkgs.jsl-zsh` in `environment.systemPackages` or `home.packages` so the wrapped shell is available directly.
`modules/services/ssh.nix` includes host entries that use `RemoteCommand = "~/.nix-profile/bin/jsl-zsh"` (and commented variants). This path-level usage means:
- keep the wrapper executable name stable (`jsl-zsh`),
- update SSH remote command references in the same change if shell invocation paths are changed.
## Change guidelines
When editing this area:
- Keep wrapper behavior changes in `packages.jsl-zsh` focused and centralized.
- Keep Home Manager zsh package selection pointed at wrapped `jsl-zsh`.
- Keep system login shell semantics (`pkgs.zsh`) and interactive wrapper semantics (`jsl-zsh`) separated unless intentionally redesigning both layers.
- If modifying `shell-tools`, verify it still composes zsh and tool packages for users importing the module.
- If changing executable names or invocation paths, update all dependent consumers (including SSH remote commands and host package lists) in one coherent change.
+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:
+12
View File
@@ -16,6 +16,18 @@ nix flake show --all-systems
nix run nixpkgs#nh home switch -- --configuration john@john-pc-ubuntu . nix run nixpkgs#nh home switch -- --configuration john@john-pc-ubuntu .
``` ```
## Host Intent
| Host key | Type | Intent | Entrypoint |
| -------- | ---- | ------ | ---------- |
| `janus` | NixOS (LXC) | Base homelab/service host with local step-ca trust bootstrap (`janus-ca`) and mTLS support. | `modules/hosts/janus/default.nix` |
| `john-kde` | Home Manager (generic Linux) | KDE-focused user environment profile for `john` with desktop + docker + step client tooling. | `modules/hosts/john-kde/default.nix` |
| `john-p14s` | NixOS (laptop) | Main ThinkPad P14s NixOS workstation using hardware presets and desktop/dev modules. | `modules/hosts/john-p14s/default.nix` |
| `john-pc-ubuntu` | Home Manager (generic Linux) | Ubuntu-hosted home profile for `john`, includes desktop/dev tooling and restic backup client settings. | `modules/hosts/john-pc/default.nix` |
| `omen-nixos` | NixOS | Omen machine wired through shared `omen-nixos` + `john` modules. | `modules/hosts/omen-nixos/default.nix` |
| `soteria` | NixOS + Home Manager | Service node running Forgejo + restic server with mTLS and host-local secrets. | `modules/hosts/soteria/soteria.nix` |
| `test-nix` | NixOS (LXC) | Minimal sandbox/test target for validating module composition and remote push/switch flows. | `modules/hosts/test-nix.nix` |
## Layout ## Layout
- Everything under `./modules` gets auto-imported by `import-tree` - Everything under `./modules` gets auto-imported by `import-tree`
Generated
+39 -424
View File
@@ -1,99 +1,6 @@
{ {
"nodes": { "nodes": {
"cachix": {
"inputs": {
"devenv": [
"devenv"
],
"flake-compat": [
"devenv",
"flake-compat"
],
"git-hooks": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1777487137,
"narHash": "sha256-TuvKVBX60mqyMT6OB5JqVEh1YIWtFMR/igLCaCdC9tw=",
"owner": "cachix",
"repo": "cachix",
"rev": "a66a440c321d35f7193472c317f42a55ccd1cb93",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"crate2nix": {
"flake": false,
"locked": {
"lastModified": 1772186516,
"narHash": "sha256-8s28pzmQ6TOIUzznwFibtW1CMieMUl1rYJIxoQYor58=",
"owner": "rossng",
"repo": "crate2nix",
"rev": "ba5dd398e31ee422fbe021767eb83b0650303a6e",
"type": "github"
},
"original": {
"owner": "rossng",
"repo": "crate2nix",
"rev": "ba5dd398e31ee422fbe021767eb83b0650303a6e",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"crate2nix": "crate2nix",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"ghostty": "ghostty",
"git-hooks": "git-hooks",
"nix": "nix",
"nixd": "nixd",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1781195293,
"narHash": "sha256-C9OFghpvf3RzK2rGsZjjNNrTrHgFOecEkpDhFnU4QGs=",
"owner": "cachix",
"repo": "devenv",
"rev": "5f5109c83854577191634f7b86fc6e0c8fd44964",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": { "flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1777699697, "lastModified": 1777699697,
@@ -111,11 +18,11 @@
}, },
"flake-file": { "flake-file": {
"locked": { "locked": {
"lastModified": 1781217157, "lastModified": 1784255282,
"narHash": "sha256-N3q/SP2Ropk336e9KSgLh7kpROY6P70dprYdbPIfd5c=", "narHash": "sha256-srM+PTqxfyvbQ0BevsOtP8vQv/Ox6OMtY4DktoDxLKg=",
"owner": "vic", "owner": "vic",
"repo": "flake-file", "repo": "flake-file",
"rev": "ce63eaf7ebfe04a176653f66385a7f0a36380cee", "rev": "66ddd2f69a5c4677f0095c6f70eea1217dc45749",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -127,57 +34,15 @@
"flake-parts": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": [ "nixpkgs-lib": [
"devenv",
"nixpkgs" "nixpkgs"
] ]
}, },
"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"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": [
"nvf",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -204,80 +69,16 @@
"type": "github" "type": "github"
} }
}, },
"ghostty": {
"flake": false,
"locked": {
"lastModified": 1779069789,
"narHash": "sha256-ojo+gso45/6CVSuqfSVnlWpQ4d0QeLgwok+v/g3yu0E=",
"owner": "ghostty-org",
"repo": "ghostty",
"rev": "4b7bf0b20e3baf9c1ba10c63f2ad1fd853faea8f",
"type": "github"
},
"original": {
"owner": "ghostty-org",
"repo": "ghostty",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778507602,
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1781365335, "lastModified": 1784351324,
"narHash": "sha256-zqDBhXMzfbdlO7F2bGHe7MOtB3xngd/+4ieMHDC+ZXo=", "narHash": "sha256-By+kuRJZRqs2TuXgtR8vJ8cTKWXw33YG/Yollu5cO1U=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "5b6f5733726a1b2ccafb5dec6ac4ca7299fad66c", "rev": "460108009ca1ff69ca2ff19079ca2c838d6e3080",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -288,11 +89,11 @@
}, },
"import-tree": { "import-tree": {
"locked": { "locked": {
"lastModified": 1778781969, "lastModified": 1784254960,
"narHash": "sha256-Jjuz5CmSkur8KvLDoGa+vylEp+RkQtv4mt/qcMznpH0=", "narHash": "sha256-iI88R3wHz8wTKQb5orvpc51L/Xr64AJyxid/0MKa/b8=",
"owner": "vic", "owner": "vic",
"repo": "import-tree", "repo": "import-tree",
"rev": "d321337efd0f23a9eb14a42adb7b2c29313ab274", "rev": "4ebb10ae17d5f1ad366e7aef5b92cb8eecf24f69",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -316,94 +117,6 @@
"type": "github" "type": "github"
} }
}, },
"ndg": {
"inputs": {
"nixpkgs": [
"nvf",
"nixpkgs"
]
},
"locked": {
"lastModified": 1779233504,
"narHash": "sha256-YIKEyzh0NFQlD0O92LQQNMoVCDwV8yw1Xz0Iu+4ZC5U=",
"owner": "feel-co",
"repo": "ndg",
"rev": "86f6644411a64d5413711895b7cf6e0e1be465b6",
"type": "github"
},
"original": {
"owner": "feel-co",
"ref": "refs/tags/v2.8.0",
"repo": "ndg",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-parts": [
"devenv",
"flake-parts"
],
"git-hooks-nix": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
]
},
"locked": {
"lastModified": 1779748925,
"narHash": "sha256-meIhqGC04O5VXbKSFXSQoOKp+XCq5RMnwAk1Guo0VQo=",
"owner": "cachix",
"repo": "nix",
"rev": "0bc443c8ff235c3547d09327b48aaa2ab98b15f2",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "devenv-2.34",
"repo": "nix",
"type": "github"
}
},
"nixd": {
"inputs": {
"flake-parts": [
"devenv",
"flake-parts"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1778381404,
"narHash": "sha256-FqhdOTA8vyoIpkHhbs2cCT7h6EWM7nsLeOYJc1ifQLE=",
"owner": "nix-community",
"repo": "nixd",
"rev": "e3e45eb76663f522e196b7f0cf34cab201db7779",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixd",
"type": "github"
}
},
"nixgl": { "nixgl": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
@@ -428,11 +141,11 @@
"nixos-hardware": { "nixos-hardware": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1781168557, "lastModified": 1784310968,
"narHash": "sha256-LOnLQ2tpYF9gqIDDr3+j3DbpJJr/QCH6zPRT2GzEUOE=", "narHash": "sha256-rkSPTePrKqs4dg+i7ZFCq93+HrClac6oSwXX927SVjA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "6358ff76821101c178e3ab4919a62799bfe3652e", "rev": "779c32a00155994c86cde8213a8dd4df139d4355",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -442,32 +155,12 @@
} }
}, },
"nixpkgs": { "nixpkgs": {
"inputs": {
"nixpkgs-src": "nixpkgs-src"
},
"locked": { "locked": {
"lastModified": 1778507786, "lastModified": 1783604885,
"narHash": "sha256-HzSQCKMsMr8r55LwM1JuzIOB+8bzk0FEv6sItKvsfoY=", "narHash": "sha256-tzMgSkV7kljEkqIjlgV6F+n+xD+/a35Db8bs7a4BFAo=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "8f24a228a782e24576b155d1e39f0d914b380691",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs-src": {
"flake": false,
"locked": {
"lastModified": 1778274207,
"narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7", "rev": "767b0d3ec98a143ad9ed7dfc0d5553510ac27133",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -479,27 +172,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1780365719, "lastModified": 1784347607,
"narHash": "sha256-QfWfccTN+70ZQ4m2qlU9PiKfz2Yppq94058iJyARNwc=", "narHash": "sha256-oJoPGsmXO7Xm5YagCr0La+dSTu5Lpfx35WN8vNG18BU=",
"owner": "NixOS", "rev": "31cd72fdba8fa052e437ce7e6879c4fe62def10f",
"repo": "nixpkgs",
"rev": "ffa10e26ae11d676b2db836259889f1f571cb14f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1781359544,
"narHash": "sha256-X8fdRtvrm8OHLZ6Lkg3ZAQm5N6we5mLkdYd92vAw4c8=",
"rev": "9f11f828c213641c2369a9f1fa31fe31557e3156",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1015979.9f11f828c213/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1036759.31cd72fdba8f/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@@ -508,21 +185,18 @@
}, },
"nvf": { "nvf": {
"inputs": { "inputs": {
"flake-compat": "flake-compat_2", "flake-compat": "flake-compat",
"flake-parts": "flake-parts_3",
"mnw": "mnw", "mnw": "mnw",
"ndg": "ndg",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ]
"systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1781440932, "lastModified": 1784320861,
"narHash": "sha256-aPOboQRLIpNe5XiNE6z3Bo0ra/l9Bgh1/vNy+wlw2v4=", "narHash": "sha256-sPUghbl8wMmESus7KvsmHIUEyD2mi13/xu7/u2H5SZU=",
"owner": "notashelf", "owner": "notashelf",
"repo": "nvf", "repo": "nvf",
"rev": "cc5fe0bdf72aad87a140b16a65296db92a9ecffb", "rev": "43fdcbf2e00290479d3f9b97b7f61edd08c4e584",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -533,41 +207,19 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"devenv": "devenv",
"flake-file": "flake-file", "flake-file": "flake-file",
"flake-parts": "flake-parts_2", "flake-parts": "flake-parts",
"home-manager": "home-manager", "home-manager": "home-manager",
"import-tree": "import-tree", "import-tree": "import-tree",
"nixgl": "nixgl", "nixgl": "nixgl",
"nixos-hardware": "nixos-hardware", "nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs_3", "nixpkgs": "nixpkgs_2",
"nvf": "nvf", "nvf": "nvf",
"sops-nix": "sops-nix", "sops-nix": "sops-nix",
"wrapper-modules": "wrapper-modules", "wrapper-modules": "wrapper-modules",
"wrappers": "wrappers" "wrappers": "wrappers"
} }
}, },
"rust-overlay": {
"inputs": {
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1779074409,
"narHash": "sha256-6aXy8Ga41iLVM8ibddFU1O5+wYWcBGNEfZzZuL91eIc=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2a77b5b1dc952f214e8102acdef1622b68515560",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"sops-nix": { "sops-nix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -575,11 +227,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1780547341, "lastModified": 1783174389,
"narHash": "sha256-Gq8KNx5A7hBB3uGJaj6eQfLDIz5YdLu92gqBcvHvoUo=", "narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "9ed65852b6257fbeae4355bc24ecfea307ca759a", "rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -603,43 +255,6 @@
"type": "github" "type": "github"
} }
}, },
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"devenv",
"nixd",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775636079,
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"wrapper-modules": { "wrapper-modules": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -647,11 +262,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1780661205, "lastModified": 1782135443,
"narHash": "sha256-3F5DixT3Gk91lBI9E+TGMm0ko5HrRbDiL23di16TJGA=", "narHash": "sha256-vAmbArdCyjqpVW+37aCy/PMBOLIqukUXLQuEKLwUhA4=",
"owner": "BirdeeHub", "owner": "BirdeeHub",
"repo": "nix-wrapper-modules", "repo": "nix-wrapper-modules",
"rev": "8dd304c3582ddd339217e1cc5fb53f50acb63c2d", "rev": "6e7f66fa2cdf4d63162580b438f7fcf87c28a46f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -667,11 +282,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1780428045, "lastModified": 1784213309,
"narHash": "sha256-pMJOun/oYhXqes+B3khzRDGWuiCNiWJ25+SVW0OwzNY=", "narHash": "sha256-A9C90KsA6FsRkFPTA1C1sHP13d6AMuGOvsvI7qjHz8g=",
"owner": "lassulus", "owner": "lassulus",
"repo": "wrappers", "repo": "wrappers",
"rev": "ce9122bf06697ade7c5087940e0c280b89dd0208", "rev": "a04c2ee17fb9679198d0a852ad8709bc152dfac2",
"type": "github" "type": "github"
}, },
"original": { "original": {
-1
View File
@@ -4,7 +4,6 @@
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules); outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);
inputs = { inputs = {
devenv.url = "github:cachix/devenv";
flake-file.url = "github:vic/flake-file"; flake-file.url = "github:vic/flake-file";
flake-parts = { flake-parts = {
url = "github:hercules-ci/flake-parts"; url = "github:hercules-ci/flake-parts";
+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 -1
View File
@@ -15,5 +15,5 @@
# no need to redefine it in your config for now) # no need to redefine it in your config for now)
# media-session.enable = true; # media-session.enable = true;
}; };
} };
} }
+2
View File
@@ -1,6 +1,7 @@
# This module is for programs with GUIs that run in a desktop environment # This module is for programs with GUIs that run in a desktop environment
{ self, inputs, ... }: { { self, inputs, ... }: {
flake.modules.homeManager.desktop = { config, pkgs, lib, ... }: { flake.modules.homeManager.desktop = { config, pkgs, lib, ... }: {
nixpkgs.config.problems.handlers.sublimetext4.broken = "ignore";
imports = with inputs.self.modules.homeManager; [ imports = with inputs.self.modules.homeManager; [
brave brave
ghostty ghostty
@@ -13,6 +14,7 @@
proton-vpn proton-vpn
joplin-desktop joplin-desktop
discord discord
github-copilot-cli
]; ];
}; };
} }
+164 -39
View File
@@ -3,6 +3,18 @@
let let
cfg = config.forgejo; cfg = config.forgejo;
needsPrivilegedPort = cfg.port < 1024; needsPrivilegedPort = cfg.port < 1024;
wrappedForgejo = (inputs.self.wrappers.forgejo.forgejo.apply {
inherit pkgs;
serviceConfig = config.services.forgejo;
}).wrapper;
cleanForgejo = (inputs.self.wrappers.forgejo.clean.apply {
inherit pkgs;
serviceConfig = config.services.forgejo;
}).wrapper;
cleanPostgres = (inputs.self.wrappers.forgejo.clean-postgres.apply {
inherit pkgs;
serviceConfig = config.services.postgresql;
}).wrapper;
in in
{ {
options.forgejo = { options.forgejo = {
@@ -11,6 +23,23 @@
type = lib.types.str; type = lib.types.str;
}; };
pocketId = {
enable = lib.mkEnableOption "Configure Pocket ID as a Forgejo OIDC provider";
discoveryUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://id.example.com";
description = "OpenID Connect discovery URL for Pocket ID.";
};
name = lib.mkOption {
type = lib.types.str;
default = "pocket-id";
description = "Forgejo external auth source name for Pocket ID.";
};
};
port = lib.mkOption { port = lib.mkOption {
type = lib.types.port; type = lib.types.port;
default = 3000; default = 3000;
@@ -27,8 +56,20 @@
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !cfg.pocketId.enable || cfg.pocketId.discoveryUrl != null;
message = "forgejo.pocketId.discoveryUrl must be set when forgejo.pocketId.enable is true.";
}
];
networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ]; networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ];
mtls = lib.mkIf cfg.https {
certReaders = lib.mkAfter [ config.services.forgejo.user ];
renew.reloadUnits = lib.mkAfter [ "forgejo.service" ];
};
systemd.services.forgejo.serviceConfig = lib.mkIf needsPrivilegedPort { systemd.services.forgejo.serviceConfig = lib.mkIf needsPrivilegedPort {
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
@@ -40,6 +81,9 @@
"forgejo/internal_token".owner = config.services.forgejo.user; "forgejo/internal_token".owner = config.services.forgejo.user;
"forgejo/jwt_secret".owner = config.services.forgejo.user; "forgejo/jwt_secret".owner = config.services.forgejo.user;
"forgejo/lfs_jwt_secret".owner = config.services.forgejo.user; "forgejo/lfs_jwt_secret".owner = config.services.forgejo.user;
} // lib.optionalAttrs cfg.pocketId.enable {
"forgejo/pocket_id/client_id".owner = config.services.forgejo.user;
"forgejo/pocket_id/client_secret".owner = config.services.forgejo.user;
}; };
services = { services = {
@@ -48,9 +92,7 @@
lfs.enable = true; lfs.enable = true;
database.type = "postgres"; database.type = "postgres";
settings = { settings = {
DEFAULT = { DEFAULT.RUN_MODE = "dev";
RUN_MODE = "dev";
};
server = lib.mkMerge [ server = lib.mkMerge [
{ {
HTTP_PORT = cfg.port; HTTP_PORT = cfg.port;
@@ -71,13 +113,9 @@
ACCOUNT_LINKING = "login"; ACCOUNT_LINKING = "login";
REGISTER_EMAIL_CONFIRM = false; REGISTER_EMAIL_CONFIRM = false;
}; };
repository = { repository.ENABLE_PUSH_CREATE_USER = true;
ENABLE_PUSH_CREATE_USER = true;
};
ui.SHOW_USER_EMAIL = false; ui.SHOW_USER_EMAIL = false;
markup = { markup.ENABLED = true;
ENABLED = true;
};
}; };
secrets = { secrets = {
@@ -109,39 +147,126 @@
--work-path ${config.services.forgejo.dump.backupDir} --work-path ${config.services.forgejo.dump.backupDir}
''; '';
environment.systemPackages = systemd.services.forgejo-pocket-id-oidc = lib.mkIf cfg.pocketId.enable {
let description = "Ensure Pocket ID OIDC auth source exists in Forgejo";
systemctl = lib.getExe' pkgs.systemd "systemctl"; after = [ "forgejo.service" "network-online.target" ];
clean-forgejo = (pkgs.writeShellScriptBin "clean-forgejo" '' wants = [ "network-online.target" ];
requires = [ "forgejo.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
User = config.services.forgejo.user;
Group = config.services.forgejo.group;
WorkingDirectory = config.services.forgejo.stateDir;
Restart = "on-failure";
RestartSec = "30s";
StartLimitIntervalSec = 0;
};
script = ''
set -euo pipefail set -euo pipefail
sudo ${systemctl} stop forgejo.service
${lib.getExe' pkgs.coreutils "echo"} "Stopped Forgejo" client_id="$(<${config.sops.secrets."forgejo/pocket_id/client_id".path})"
sudo ${lib.getExe' pkgs.coreutils "rm"} -rf ${config.services.forgejo.stateDir} client_secret="$(<${config.sops.secrets."forgejo/pocket_id/client_secret".path})"
${lib.getExe' pkgs.coreutils "echo"} "Removed ${config.services.forgejo.stateDir}" provider_name="${cfg.pocketId.name}"
''); forgejo_bin="${lib.getExe wrappedForgejo}"
clean-postgres = (pkgs.writeShellScriptBin "clean-postgres" ''
set -euo pipefail auth_id="$(
sudo ${systemctl} stop postgresql.service "$forgejo_bin" admin auth list \
${lib.getExe' pkgs.coreutils "echo"} "Stopped PostgreSQL" | ${lib.getExe' pkgs.gawk "awk"} '$2 == "'"$provider_name"'" { print $1; exit }'
sudo ${lib.getExe' pkgs.coreutils "rm"} -rf ${config.services.postgresql.dataDir} )"
${lib.getExe' pkgs.coreutils "echo"} "Removed ${config.services.postgresql.dataDir}"
''); if [ -n "$auth_id" ]; then
in [ "$forgejo_bin" admin auth update-oauth \
clean-forgejo --id "$auth_id" \
clean-postgres --name "$provider_name" \
(pkgs.writeShellScriptBin "clean-all" '' --provider openidConnect \
set -euo pipefail --key "$client_id" \
GREEN_CHECK="\e[32m\e[0m" --secret "$client_secret" \
YELLOW_BANG="\e[33m!\e[0m" --auto-discover-url "${cfg.pocketId.discoveryUrl}" \
${lib.getExe' pkgs.coreutils "echo"} -n -e "$YELLOW_BANG Remove everything related to Forgejo and the PostgreSQL database behind it?" --scopes openid \
read -p " (y/n) " -n 1 -r --scopes profile \
if [[ $REPLY =~ ^[Yy]$ ]]; then --scopes email
${lib.getExe clean-forgejo} else
${lib.getExe clean-postgres} "$forgejo_bin" admin auth add-oauth \
${lib.getExe' pkgs.coreutils "echo"} -e "$GREEN_CHECK Removed everything related to forgejo" --name "$provider_name" \
--provider openidConnect \
--key "$client_id" \
--secret "$client_secret" \
--auto-discover-url "${cfg.pocketId.discoveryUrl}" \
--scopes openid \
--scopes profile \
--scopes email
fi fi
'') '';
};
environment.systemPackages = [
wrappedForgejo
cleanForgejo
cleanPostgres
]; ];
}; };
}; };
flake.wrappers.forgejo = {
forgejo = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
options = {
serviceConfig = lib.mkOption { type = lib.types.attrs; };
};
config = let
cfg = config.serviceConfig;
forgejo_config="${cfg.customDir}/conf/app.ini";
forgejo_work_path="${cfg.stateDir}";
in {
binName = "my-forgejo";
package = cfg.package;
args = [
"--config" forgejo_config
"--work-path" forgejo_work_path
];
};
});
clean = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
options = {
serviceConfig = lib.mkOption { type = lib.types.attrs; };
};
config = let
cfg = config.serviceConfig;
pkgs = config.pkgs;
in {
binName = "forgejo-clean";
package = pkgs.coreutils;
exePath = lib.getExe' pkgs.coreutils "rm";
extraPackages = [ pkgs.systemd ];
preHook = ''
sudo systemctl stop forgejo.service
echo "Stopped Forgejo"
'';
args = [ "-rf" cfg.stateDir ];
postHook = ''echo "Removed ${cfg.stateDir}"'';
};
});
clean-postgres = inputs.wrappers.lib.wrapModule ({ config, lib, wlib, ... }: {
options = {
serviceConfig = lib.mkOption { type = lib.types.attrs; };
};
config = let
cfg = config.serviceConfig;
pkgs = config.pkgs;
in {
binName = "postgres-clean";
package = pkgs.coreutils;
exePath = lib.getExe' pkgs.coreutils "rm";
extraPackages = [ pkgs.systemd ];
preHook = ''
sudo systemctl stop postgresql.service
echo "Stopped Postgres"
'';
args = [ "-rf" cfg.dataDir ];
postHook = ''echo "Removed ${cfg.dataDir}"'';
};
});
};
} }
-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 = "";
};
};
});
};
}
+54
View File
@@ -0,0 +1,54 @@
{ 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";
};
certGroup = lib.mkOption {
description = "Group that should be granted read access to generated certificate material.";
type = lib.types.str;
default = "mtls";
};
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" ];
};
};
};
}
+102
View File
@@ -0,0 +1,102 @@
{ 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}"
chgrp "${config.certGroup}" "${config.certDir}"
chmod 0750 "${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 = ''
chgrp "${config.certGroup}" "${config.certFile}" "${config.keyFile}"
chmod 0640 "${config.certFile}" "${config.keyFile}"
(umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}")
chgrp "${config.certGroup}" "${config.bundleFile}"
chmod 0640 "${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 = ''
chgrp "${config.certGroup}" "${config.certFile}" "${config.keyFile}"
chmod 0640 "${config.certFile}" "${config.keyFile}"
(umask 077; cat "${config.certFile}" "${config.keyFile}" > "${config.bundleFile}")
chgrp "${config.certGroup}" "${config.bundleFile}"
chmod 0640 "${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"
];
};
});
};
}
+1
View File
@@ -8,6 +8,7 @@
nixpkgs.config = { nixpkgs.config = {
permittedInsecurePackages = [ "openssl-1.1.1w" ]; permittedInsecurePackages = [ "openssl-1.1.1w" ];
allowUnfree = true; allowUnfree = true;
problems.handlers.sublimetext4.broken = "ignore";
}; };
# Set your time zone. # Set your time zone.
+59 -4
View File
@@ -1,10 +1,65 @@
{ self, inputs, ... }: { { self, inputs, ... }: {
flake.modules.nixos.restic-server = { config, pkgs, lib, ... }: { flake.modules.nixos.restic-server = { config, pkgs, lib, ... }:
let
cfg = config.resticServer;
port = builtins.fromJSON (lib.last (lib.splitString ":" cfg.listenAddress));
in {
options.resticServer = {
enable = lib.mkEnableOption "Enable the RESTic REST server";
dataDir = lib.mkOption {
description = "Storage directory for RESTic repositories served by this host.";
type = lib.types.str;
default = "/mnt/restic";
};
listenAddress = lib.mkOption {
description = "Listen address for the RESTic REST server.";
type = lib.types.str;
default = "0.0.0.0:8080";
};
privateRepos = lib.mkOption {
description = "Whether the RESTic server should use private repository mode.";
type = lib.types.bool;
default = true;
};
extraFlags = lib.mkOption {
description = "Additional flags to pass to the RESTic REST server before TLS flags are appended.";
type = lib.types.listOf lib.types.str;
default = [ "--no-auth" ];
};
tls = {
certFile = lib.mkOption {
description = "Path to the TLS certificate file for the RESTic REST server, or null to disable TLS.";
type = lib.types.nullOr lib.types.str;
default = null;
};
keyFile = lib.mkOption {
description = "Path to the TLS private key file for the RESTic REST server, or null to disable TLS.";
type = lib.types.nullOr lib.types.str;
default = null;
};
};
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [ port ];
services.restic.server = { services.restic.server = {
enable = true; enable = true;
dataDir = "/mnt/restic"; inherit (cfg) dataDir listenAddress privateRepos;
listenAddress = "0.0.0.0:8080"; extraFlags =
extraFlags = [ "--no-auth" ]; cfg.extraFlags
++ lib.optionals (cfg.tls.certFile != null && cfg.tls.keyFile != null) [
"--tls"
"--tls-cert=${cfg.tls.certFile}"
"--tls-key=${cfg.tls.keyFile}"
];
};
}; };
}; };
-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}"
];
};
});
}
+136 -19
View File
@@ -1,29 +1,146 @@
{ self, inputs, ... }: { { self, inputs, ... }:
flake.modules.homeManager.step-client = { config, pkgs, lib, ... }: { let
home.file.".step/config/defaults.json".text = builtins.toJSON { defaultCaUrl = "https://janus.john-stream.com/";
ca-url = "https://janus.john-stream.com/"; defaultFingerprint = "2036c44f7b5901566ff7611ea6c927291ecc6d2dd00779c0eead70ec77fa10d6";
fingerprint = "2036c44f7b5901566ff7611ea6c927291ecc6d2dd00779c0eead70ec77fa10d6"; defaultRoot = ../hosts/janus/public/root_ca.crt;
root = ../hosts/janus/root_ca.crt;
mkStepBootstrap = { pkgs, caUrl, fingerprint, install ? false }:
(inputs.self.wrappers.stepBootstrap.apply {
inherit pkgs install;
ca-url = caUrl;
inherit fingerprint;
}).wrapper;
mkDefaultsText = cfg: builtins.toJSON {
ca-url = cfg.caUrl;
fingerprint = cfg.fingerprint;
root = cfg.root;
}; };
home.packages = [ in
inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.step-bootstrap {
flake.modules.nixos.step-client = { config, pkgs, lib, ... }:
let
cfg = config."step-client";
in
{
options."step-client" = {
enable = lib.mkOption {
description = "Enable step-ca client bootstrap";
type = lib.types.bool;
default = true;
};
caUrl = lib.mkOption {
description = "The step-ca URL used for bootstrap and renewal.";
type = lib.types.str;
default = defaultCaUrl;
};
fingerprint = lib.mkOption {
description = "The SHA256 fingerprint of the step-ca root certificate.";
type = lib.types.str;
default = defaultFingerprint;
};
root = lib.mkOption {
description = "The step-ca root certificate used for bootstrap.";
type = lib.types.path;
default = defaultRoot;
};
certDir = lib.mkOption {
description = "Directory used to store root CA material for mTLS and step bootstrap.";
type = lib.types.str;
default = "/etc/step-ca/certs";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
(mkStepBootstrap {
inherit pkgs;
caUrl = cfg.caUrl;
fingerprint = cfg.fingerprint;
})
]; ];
# sops.secrets."step-ca-defaults" = {
# sopsFile = ../hosts/janus/defaults.json; environment.etc."step-ca/defaults.json".text = mkDefaultsText cfg;
# format = "json";
# key = ""; # This causes it to decode the whole file systemd.tmpfiles.rules = [
# path = "${config.home.homeDirectory}/defaults.json"; "d ${cfg.certDir} 0750 root root -"
# mode = "0400"; "L+ ${cfg.certDir}/root_ca.crt - - - - ${cfg.root}"
# }; "d /root/.step 0700 root root -"
"d /root/.step/config 0700 root root -"
"d /root/.step/certs 0700 root root -"
"L+ /root/.step/config/defaults.json - - - - /etc/step-ca/defaults.json"
"L+ /root/.step/certs/root_ca.crt - - - - ${cfg.certDir}/root_ca.crt"
];
};
};
flake.modules.homeManager.step-client = { config, pkgs, lib, ... }:
let
cfg = config."step-client";
in
{
options."step-client" = {
enable = lib.mkOption {
description = "Enable step-ca client bootstrap";
type = lib.types.bool;
default = true;
};
caUrl = lib.mkOption {
description = "The step-ca URL used for bootstrap and renewal.";
type = lib.types.str;
default = defaultCaUrl;
};
fingerprint = lib.mkOption {
description = "The SHA256 fingerprint of the step-ca root certificate.";
type = lib.types.str;
default = defaultFingerprint;
};
root = lib.mkOption {
description = "The step-ca root certificate used for bootstrap.";
type = lib.types.path;
default = defaultRoot;
};
certDir = lib.mkOption {
description = "Directory used to store root CA material for mTLS and step bootstrap.";
type = lib.types.str;
default = "${config.home.homeDirectory}/.step/certs";
};
};
config = lib.mkIf cfg.enable (
let
certDirPath = lib.removePrefix "${config.home.homeDirectory}/" cfg.certDir;
in
{
home.packages = [
(mkStepBootstrap {
inherit pkgs;
caUrl = cfg.caUrl;
fingerprint = cfg.fingerprint;
})
];
home.file.".step/config/defaults.json".text = mkDefaultsText cfg;
home.file."${certDirPath}/root_ca.crt".source = cfg.root;
}
);
}; };
perSystem = { system, pkgs, lib, ... }: { perSystem = { system, pkgs, lib, ... }: {
packages.step-bootstrap = (inputs.self.wrappers.stepBootstrap.apply { packages.step-bootstrap = mkStepBootstrap {
inherit pkgs; inherit pkgs;
ca-url = "https://janus.john-stream.com"; caUrl = defaultCaUrl;
fingerprint = "2036c44f7b5901566ff7611ea6c927291ecc6d2dd00779c0eead70ec77fa10d6"; fingerprint = defaultFingerprint;
install = true; install = true;
}).wrapper; };
}; };
flake.wrappers.stepBootstrap = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: { flake.wrappers.stepBootstrap = inputs.wrappers.lib.wrapModule ({config, lib, wlib, ... }: {
+85
View File
@@ -0,0 +1,85 @@
# Janus
Generate passwords:
```shell
mkdir -p /tmp/janus-step-ca-bootstrap && chmod 700 /tmp/janus-step-ca-bootstrap && cd /tmp/janus-step-ca-bootstrap && umask 077 && openssl rand -base64 48 > ca_password.txt && openssl rand -base64 48 > admin_jwk_password.txt
```
Generate the Janus OpenSSH host key for reference (Janus now uses whatever key
already exists on the target at `/etc/ssh/ssh_host_ed25519_key`):
```shell
ssh-keygen -t ed25519 -N '' -C [email protected] -f /tmp/janus-step-ca-bootstrap/ssh_host_ed25519_key
```
Bootstrap CA materials with SSH enabled:
```shell
STEPPATH=/tmp/janus-step-ca-bootstrap/step step ca init --name Janus --dns janus.john-stream.com --dns 192.168.1.244 --address :443 --provisioner admin --password-file /tmp/janus-step-ca-bootstrap/ca_password.txt --provisioner-password-file /tmp/janus-step-ca-bootstrap/admin_jwk_password.txt --ssh --deployment-type standalone --with-ca-url https://janus.john-stream.com
```
Insert generated runtime CA material into `modules/hosts/janus/secrets.yaml` under `janus`:
- `/tmp/janus-step-ca-bootstrap/ca_password.txt` -> `ca_password`
- `/tmp/janus-step-ca-bootstrap/step/secrets/intermediate_ca_key` -> `intermediate_ca_key`
- `/tmp/janus-step-ca-bootstrap/step/secrets/ssh_host_ca_key` -> `ssh_host_ca_key`
- `/tmp/janus-step-ca-bootstrap/step/secrets/ssh_user_ca_key` -> `ssh_user_ca_key`
- `/tmp/janus-step-ca-bootstrap/step/config/ca.json` -> `admin_provisioner_encrypted_key` (copy `authority.provisioners[].encryptedKey` for the `admin` JWK provisioner)
If you are only validating wiring first, `admin_provisioner_encrypted_key` can be an encrypted placeholder and replaced later.
If rotating provisioner password, also set:
- `/tmp/janus-step-ca-bootstrap/admin_jwk_password.txt` -> `janus.admin_jwk` in `keys/secrets.yaml`
Secret source-of-truth after this split:
- `modules/hosts/janus/secrets.yaml`: Janus runtime CA secrets (`ca_password`, `intermediate_ca_key`, `ssh_host_ca_key`, `ssh_user_ca_key`, `admin_provisioner_encrypted_key`)
- `keys/secrets.yaml`: shared Janus provisioner secret (`janus.admin_jwk`) consumed 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
```
+42 -73
View File
@@ -2,94 +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;
certRootEtcPath = "${certDirEtcPath}/root_ca.crt";
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 = builtins.readFile ./fingerprint;
root = "/etc/${certRootEtcPath}";
};
"${certRootEtcPath}".source = ./root_ca.crt;
};
systemd.tmpfiles.rules =
mkStepRules johnHome username johnGroup
++ mkStepRules "/root" "root" "root";
};
};
flake.modules.homeManager.janus-ca = { config, ... }: {
home.file.".step/config/defaults.json".text = builtins.toJSON {
ca-url = "https://janus.john-stream.com/";
fingerprint = builtins.readFile ./fingerprint;
root = ./root_ca.crt;
};
};
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;
step-ssh-host = { loginText.extraServiceStatus = {
hostname = hostname; "Step-CA" = "step-ca";
}; };
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
];
shell.program = "zsh";
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=
+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
+11 -6
View File
@@ -1,7 +1,7 @@
{ withSystem, self, inputs, ... }: { withSystem, self, inputs, ... }:
let let
username = "john"; username = "john";
hostname = "omen"; hostname = "john-kde";
in in
{ {
flake.modules.homeManager."${hostname}" = { config, pkgs, lib, ... }: flake.modules.homeManager."${hostname}" = { config, pkgs, lib, ... }:
@@ -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
@@ -62,9 +61,15 @@ in
}; };
}; };
flake.homeConfigurations."john@omen" = withSystem "x86_64-linux" (ctx@{ system, inputs', ... }: flake.homeConfigurations."john@john-kde" = inputs.home-manager.lib.homeManagerConfiguration {
inputs.home-manager.lib.homeManagerConfiguration { pkgs = import inputs.nixpkgs.outPath {
pkgs = inputs'.nixpkgs.legacyPackages; localSystem.system = "x86_64-linux";
config = {
allowUnfree = true;
permittedInsecurePackages = [ "openssl-1.1.1w" ];
problems.handlers.sublimetext4.broken = "ignore";
};
};
modules = [ inputs.self.modules.homeManager."${hostname}" ]; # Uses the module defined above modules = [ inputs.self.modules.homeManager."${hostname}" ]; # Uses the module defined above
}); };
} }
+2 -1
View File
@@ -16,7 +16,7 @@
rebuild.flakeDir = flakeDir; rebuild.flakeDir = flakeDir;
networking.hostname = hostname; networking.hostName = hostname;
programs.zsh.enable = true; programs.zsh.enable = true;
services.tailscale.enable = true; services.tailscale.enable = true;
@@ -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
]); ]);
+40 -43
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,12 +17,13 @@ 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
mysops mysops
# zed-editor
# myPackage # myPackage
# myStepClient # myStepClient
]; ];
@@ -35,15 +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
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;
@@ -51,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;
"fded:fb16:653e:25da:be24:11ff:fea0:753f ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ9ZqiWPrCwHjxFCiu0lT4rlQs7KyMapxKJQQ5PJP1eh"
];
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;
@@ -79,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;
@@ -104,13 +99,15 @@ in
# }; # };
}; };
flake.homeConfigurations."john@john-pc-ubuntu" = withSystem "x86_64-linux" (ctx@{ system, inputs', ... }: flake.homeConfigurations."john@john-pc-ubuntu" = inputs.home-manager.lib.homeManagerConfiguration {
inputs.home-manager.lib.homeManagerConfiguration { pkgs = import inputs.nixpkgs.outPath {
# pkgs = import inputs.nixpkgs { localSystem.system = "x86_64-linux";
# inherit system; config = {
# overlays = [ inputs.self.overlays.default ]; allowUnfree = true;
# }; permittedInsecurePackages = [ "openssl-1.1.1w" ];
pkgs = inputs'.nixpkgs.legacyPackages; problems.handlers.sublimetext4.broken = "ignore";
};
};
modules = [ inputs.self.modules.homeManager."${hostname}" ]; modules = [ inputs.self.modules.homeManager."${hostname}" ];
}); };
} }
+3 -3
View File
@@ -1,9 +1,9 @@
{ self, inputs, ... }: { { self, inputs, ... }: {
flake.modules.nixos.omen = { pkgs, lib, ... }: { flake.modules.nixos.omen-nixos = { pkgs, lib, ... }: {
# import any other modules from here # import any other modules from here
imports = [ imports = [
self.modules.nixos.omenHardware self.modules.nixos.omen-nixos-hardware
self.modules.nixos.base self.modules.nixos.base
self.modules.nixos.greetd self.modules.nixos.greetd
self.modules.nixos.niri self.modules.nixos.niri
@@ -18,7 +18,7 @@
# Use latest kernel. # Use latest kernel.
boot.kernelPackages = pkgs.linuxPackages_latest; boot.kernelPackages = pkgs.linuxPackages_latest;
networking.hostName = "nixos-omen"; # Define your hostname. networking.hostName = "omen-nixos"; # Define your hostname.
# Configure network proxy if necessary # Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/"; # networking.proxy.default = "http://user:password@proxy:port/";
+2 -2
View File
@@ -1,7 +1,7 @@
{ self, inputs, ... }: { { self, inputs, ... }: {
flake.nixosConfigurations.omen = inputs.nixpkgs.lib.nixosSystem { flake.nixosConfigurations.omen-nixos = inputs.nixpkgs.lib.nixosSystem {
modules = [ modules = [
self.modules.nixos.omen self.modules.nixos.omen-nixos
self.modules.nixos.john self.modules.nixos.john
]; ];
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
{ self, inputs, ... }: { { self, inputs, ... }: {
flake.modules.nixos.omenHardware = { config, lib, pkgs, modulesPath, ... }: { flake.modules.nixos.omen-nixos-hardware = { config, lib, pkgs, modulesPath, ... }: {
imports = [ imports = [
(modulesPath + "/installer/scan/not-detected.nix") (modulesPath + "/installer/scan/not-detected.nix")
]; ];
@@ -1,25 +1,31 @@
{ withSystem, self, inputs, lib, ... }: { withSystem, self, inputs, lib, ... }:
# Intent: NixOS service node for Forgejo + restic with mTLS, host-local secrets, and a paired Home Manager profile for john.
let let
username = "john"; username = "john";
hostname = "soteria"; hostname = "soteria";
forgejoDomain = "forgejo.john-stream.com";
ipv4 = "192.168.1.233";
ipv6 = "fded:fb16:653e:25da:be24:11ff:fe54:aa39";
sshNames = [ "${hostname}.john-stream.com" ipv4 ipv6 ];
mtlsNames = sshNames ++ [ forgejoDomain ];
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 [
@@ -34,57 +40,61 @@ 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 = mtlsNames;
"${hostname}.john-stream.com" certDir = "/etc/mtls";
# "192.168.1.142" # lifetime = "12h";
"forgejo.john-stream.com" # renew.onCalendar = "*:3/15";
"192.168.1.244" # renew.reloadUnits = [ "forgejo.service" "restic-rest-server.service" ];
]; # 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 = { forgejo = {
enable = true; enable = true;
root_url = "https://forgejo.john-stream.com"; root_url = "https://forgejo.john-stream.com";
https = true; https = true;
port = 443; port = 443;
pocketId = {
enable = true;
discoveryUrl = "https://pocketid.john-stream.com/.well-known/openid-configuration";
};
}; };
networking.firewall.allowedTCPPorts = [ 8000 ]; # resticServer = {
services.restic.server = { # enable = true;
enable = true; # dataDir = "/mnt/restic";
privateRepos = true; # privateRepos = true;
listenAddress = "0.0.0.0:8000"; # listenAddress = "0.0.0.0:8000";
extraFlags = [ # tls = {
"--no-auth" # certFile = config.mtls.certFile;
"--tls" # keyFile = config.mtls.keyFile;
"--tls-cert=${config.mtls.certFile}" # };
"--tls-key=${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 = sshNames;
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 = [
@@ -96,9 +106,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,13 +119,4 @@ in
}) })
]; ];
}; };
flake.homeConfigurations.soteria = withSystem "x86_64-linux" (ctx@{ config, inputs', ...}:
inputs.home-manager.lib.homeManagerConfiguration {
pkgs = inputs'.nixpkgs.legacyPackages;
modules = [
inputs.self.modules.homeManager."${username}"
inputs.self.modules.homeManager.soteria
];
});
} }
+35 -34
View File
@@ -1,44 +1,45 @@
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:H3xdtb9TA/VStpOzLxboPTck0iasvAAS77hVFQOI1hmIwwYNYuRXTWb9N0M9kBEv/ho=,iv:PkLd3iHLzjnsPZtfQq/LJcwHQDm+gyxQJX/jWrpRmUw=,tag:qHtHvotCqr4IGs0vi5AOhw==,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:h1TOZGnV4yBX/wFVlpyxP/KhtKbjqpvxWL7/bxONfoi3PQMtkQfwWbjd/FTC72XyCAHO/yDsCtHRQjPQcyeXmw==,iv:XgdO/i8FKBCYsRfFc5JAg+9yA3TbreIJw4J8p0xif0s=,tag:TgrJREyZuc7cdUcru1Qk0w==,type:str]
#ENC[AES256_GCM,data:TADqVsZwXpH/zfrXNclavH8Sv++jX081UjK/Mkys5x52gU5fU0ikk478EmwzeergsHpmxxon,iv:v9HsIsud+eiko7F0u622MBrTVDsIxlpqru0Viifvqow=,tag:FLgNTW0EcGhHT6iI9I6Bsg==,type:comment] #ENC[AES256_GCM,data:43sjaBcdtJol6ySQKUuhZfSM4TvS6HLrjI/CLSlFcBfIYIe8UoPK6ZSsG7u3knVc58u8YreH,iv:aHaCmk5lr05PbXAKASPy22t88f//LOEVgSRzbZ6KKow=,tag:U3wsAmEwbaKUzjwdp6ekcg==,type:comment]
internal_token: ENC[AES256_GCM,data:7eZM+misgHZz1p6HH9VzCAPIIFjp2vT0kyc9B1KllE+x2URyrgs5IR9SZ+T5vNdMdnlNzF25pUhKzjbak8tVN554rYWp06SczzqhLyaoHKs8iGBohMqfJkoZxxIVSB7g7GC+/SAsb719,iv:XbBXqXh/PqjEcAj0qsajR7Ij56KGT/9Spdp5T549p9I=,tag:o4ONucOkIE2VJ6VjOlPb6w==,type:str] internal_token: ENC[AES256_GCM,data:tFzyCj02HXauxCqKFsZD0CB/W4FAxv4t0KSIZPPXV6a2N6PdDuL0W5+Wdd9O17E/Pa8mrourwjpqipON+FUNARp4bLsFp1bgBPeq52BLS1neSXC1YrxsO3W9/12Y5wpXI5mz7WwVJUvt,iv:uyxZlZ8vVnwkQBwNvPLgbeomyhx/lfSgi5xJyB2LLb8=,tag:oXfWY1wGL3NvlpfeGyIWxg==,type:str]
#ENC[AES256_GCM,data:ZqwgnKjaolJtjcy287fnDOkb/oSLnBpfWfsTeVPwbIE8YLRSoPP4gbCnHJBLq+TJNNI=,iv:zTvw4ZS6C1ifUwOijNLuTfUQ3JM+5gj1X2f/s8MwWXc=,tag:Y1yKlL+jIRHVBulGlSErog==,type:comment] #ENC[AES256_GCM,data:8sFQ0j8jmDAmnl5FD6FFtHv6t34rFVu9O658hlGktKKFDpgXnj/IIaelvqSrsSlS6nA=,iv:HWYldmocbK/auGAXB/01zmC/vC58Z/2UYla6CFoaAL8=,tag:3dhcO0ip/w54Cf2pHwXqiQ==,type:comment]
jwt_secret: ENC[AES256_GCM,data:e59MlATOorsTIQjtTUKfX5Yo3CVsbbfuKczp1gh1m2D1kkZK3ORFztYpjg==,iv:JH3PVUmXToiThEKDkDJ8MGVMAPlIEgPSWhru+9WgNjk=,tag:FfDpaCPejpw6kGDkxJwDWw==,type:str] jwt_secret: ENC[AES256_GCM,data:uIIHBk3m5DNHXRJ7pyg0Xvz1cxUFqfxUzFtBZGY59E4dPj9qHHe3RC7jEg==,iv:krZHjrU3QziSuHihpVLHaSIzHU5VZ1UUBeLBd3kWW3Y=,tag:3Z0UCV9+9nc+FU9vXFutbQ==,type:str]
lfs_jwt_secret: ENC[AES256_GCM,data:xi9PEKFUGRyc3YOg3JM3KrrENi9xsbeBjiz4R16SK5WDafoGFLazN6KRJQ==,iv:1IhPyQDwA8tZ22pfZJiU8TRTCLCHC/HAnKdmSGDfvcM=,tag:rLdREVSKBm67rt8ayN16Vw==,type:str] lfs_jwt_secret: ENC[AES256_GCM,data:LGemlQqpCwy1HlQI3BfW6Qir8cjYgtpoyFkoKhYgGR5x+F3shCaiUNEwFA==,iv:3C3S4gUpk1szU/7Dg9RObpv3/DMpzCpFg3qyrfFMot0=,tag:dwIRfA6PoQo09+YEs6o8fg==,type:str]
restic_password: ENC[AES256_GCM,data:u7QOZXJkxVG4J75K5nphb2uJGdz6jbWuVSsKKu+41fshp7cVoRijtr/Cs02LjVse,iv:bt1W2FeBTG6ypBFYzMPXPIkYTSn0uHURY2ui6MRgYY8=,tag:DObAMws/zQcM+UKUe9EECA==,type:str] pocket_id:
client_id: ENC[AES256_GCM,data:0aju1RTsGGpKRWHoBIydKUzijX5iHYWlAW/q5Ykam1WUs+py,iv:pkcZrlE1OZnEUZPnveWWkShA225tinm0eDIpmg1YbWY=,tag:vz168jIJzbJ7+DrZzn4BmA==,type:str]
client_secret: ENC[AES256_GCM,data:dXqdSqwc6aDmWOD3sYla0KWdk3CUQxLpKaQ0b5uozRI=,iv:N/LHumamZMqtqOdx3Mmj25zlsvYfriYahdxtd/goCAk=,tag:kYjcXzwrsnTN5pBGfDi1Yw==,type:str]
restic_password: ENC[AES256_GCM,data:4OOYu5+BabBtRkGjvXwIAxb+L5H3ADkqTWZFwSKIV0eOhRvNfBTlL7HBvvMCFXcP,iv:wBmht7AbnZwcKV3vBygrcuV02KoCStVn3AYt3iLOBpI=,tag:cMcCqwb6wPMm9ZKk59me4A==,type:str]
sops: sops:
age: age:
- recipient: age1ykcs39e62pz3xu6cedg8ea685kv5d5qsrhgkndygzm8rx30xd5ys5t3qxt - enc: |
enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5QThHOFdVQ0F6ZDE3QXdL YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVV2FrSURwdEozZ0l6MjRJ
QVF0WW5yRkNtNXIyRm1LeEhwY2dRRWo0WENZCmZoWmlXZDh4ZGlUR2JYOUxuaVAx VWU4YTVVMmN1OHNEWWJ1RmJrU1ltcFhGVW00CkFTWDRtYWtKSFRBcVRKZXZtUCtQ
WjhXVHBMNWdrdkgvTENJR2RpWldkNTAKLS0tIEF1bS9JSnFTbytBa1U5RjVzWkd3 Zi9pa21BcGJaMmJTa3Jab1VRUW5LVUUKLS0tIHlUY2d3OXVVTDY4UDhOamYrSnNW
S0pKVVI2RFN5a1BMYXBEY3VOUUM5QTAKNarYZm9DKRQhosSJBn9yryFxDkmFTV/o R1orWGIwUFpPVnpQRUFuNm00OEZ5MWsKwH1QahhZWw4aUbAcST+fL2HefrJdKktD
i8b/tZ1ZybjEXX+X1EsgylM5u8iKkbEyUzqKO0E0gpg4qXSsJaMMYQ== G5sLmNPa3aEKx0Y3CSIvgww23iQYw31iXJS9sVcPyMVt99YP21KbvA==
-----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+IFgyNTUxOSBReFNXY3c0UnZ3cGZnbnFO
cVB0NUlkV1JPRjQ1TW5RSVFHUjdNMERoRXlzCk9tc25iMG00TkZjWjk2S2cyVE1J K3dZS2w4S0UzeFE4YXg0V3BjbzNISm8xdWdZCkc4NUdBMktMbStSZkFtTFBHcGNQ
cUFITDh2SkZGN0lpeWJVcWM3V3JrTTQKLS0tIENOWVBzTFYrdHRsMERwV0RKS01j eDdlQXRXb3NKaUtLWXlqN0txOGxoMTgKLS0tIEZPV1BCWUFRaldIMDlSRXZzMmtN
QVI5TVl4L0dwOWpoQ1I2THZ6cWovWGMKIo1x1ZbdTyr/dNlPhvuomfk2MoPLsHyU aW14MnNtZVM0M0M0ajk2UElCWklhaVUKZewlYVMefFqC+ddzrgmAXVTiuBqhhxEL
N3CP3Scu3aZ7vqVua7uwtv4xQqyQI/yOnFjwxrVYPJ+N5Y/b4wsC6A== Q3cUhxGk5DUhCn30ia/ztQIb79kVBrdhY6zA2bKcBdYH4HSzV+rNAA==
-----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+IFgyNTUxOSBhbVRyNmN3WU5SbTNtbzZV
RjJnN0YwWFM4d1IwTElqUnZQQkVDTmdydUJjCjdCUWxCVDNXUHVLd2ZHcDA5L0Mw cGoybXo0dWtpRDNkR2drZTN0VFhoQ0REQ0ZvCkNSSm9hbmZDNUsxTjBGWm5QL3M2
cEVxVE84L2xmQnRoYmsxVEdMSEZjdGcKLS0tIHViRVJyTWt4Q3JrMWtYaS9QdHlS VElMSi8ybFRVM3hpNUlYdVAyME1RTUEKLS0tIGJVRis5WkRDRkxCVVdJVENlZ3Ru
Yjd0MUcxcExvWVpCOUR3MkdZdGQyWUkKnru0Y2A98+0Mps7EtVK7ct3vPqIGveUt aGw3cW51Vno4emFMckxqRTVGcGhoemcK4WBC2EZEx7hVlUQnsD4V5QH9peNjUoWE
E5fzpcKvdefzObrx7BPTwJ19t2fZg/dSi7HKwx3vmKZSzyQaqJOzsg== XjCvw0twhPRi9uXd4HmHqlDpxdGMk0oFQ77O7h7Q3z2uqsJ2v8p/SA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-04T23:18:43Z" recipient: age1gvplss0ddmyf6vpjy363wu3n057vhm0j6n7tc94cxd8kadapypws5mtaj0
mac: ENC[AES256_GCM,data:qBgeli5lHb4pyA8nAADBuRBAaq8VbAIsFI37OZtgnbnoHW2crxo3YC+EknaIYnZpZ48kwVhQS5lGRjI6JsWWhTH3+LVAhTmS2Qj/pZTD/JDLK6XJGXS4U9nB7m9aGYyW8gFCy9/DfoJWGsS//+ZmUikKPfd5kMZgh1zGoYCIGug=,iv:h+2fA+bO2SMCNrEslP36x3BPRaIy25cU/DNX8CYSC6A=,tag:RvVyUZ4ONRaKaqGiT31eUQ==,type:str] lastmodified: "2026-07-10T04:31:57Z"
mac: ENC[AES256_GCM,data:uytG0nhJ/Gp0KbOovD1SVXx0G27e12gMPwmpkU0lPW01cAXNe0rv4SjhHGP0FOR6vLimcPAmekVjVqiKxie8Zevo/Jf9ZRTkQ6ixWLvhH2NMHra4T5TWtUjfMdFM8tiilwcvjsXpqAoaJDZ0HHY5NYhXG/khobAwBRc26RtICVw=,iv:6gB28VE17+NoM/tR9lIlJoMPD91Aud+kH95Vo+fWLjs=,tag:QMUQPsCnr1TUsQE66GVBBg==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.12.2 version: 3.13.1
-30
View File
@@ -1,30 +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
{
home-manager.users."${username}" = {
imports = with inputs.self.modules.homeManager; [
mysops
];
shell.program = "zsh";
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 {
+16 -2
View File
@@ -1,13 +1,27 @@
{ inputs, ... }: { inputs, ... }:
{ {
flake.modules.nixos.lxc = { pkgs, lib, ...}: { flake.modules.nixos.lxc = { pkgs, lib, ...}:
let
selfPackages = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
rootShellPath = lib.getExe' selfPackages.jsl-zsh-tty "jsl-zsh-tty";
in
{
imports = with inputs.self.modules.nixos; [ imports = with inputs.self.modules.nixos; [
({ modulesPath, ... }: { imports = [ "${modulesPath}/virtualisation/proxmox-lxc.nix" ]; }) ({ modulesPath, ... }: { imports = [ "${modulesPath}/virtualisation/proxmox-lxc.nix" ]; })
]; ];
nixpkgs.hostPlatform = lib.mkForce "x86_64-linux"; nixpkgs.hostPlatform = lib.mkForce "x86_64-linux";
system.stateVersion = "25.11"; system.stateVersion = "25.11";
time.timeZone = "US/Central";
nix.settings.experimental-features = [ "nix-command" "flakes" ]; nix.settings.experimental-features = [ "nix-command" "flakes" ];
environment.systemPackages = with pkgs; [ git zsh ]; environment.systemPackages = with pkgs; [
git
selfPackages.jsl-zsh-tty
];
environment.shells = lib.mkAfter [ rootShellPath ];
users.users.root.shell = lib.mkForce rootShellPath;
networking.nameservers = [ "192.168.1.150" ];
networking.dhcpcd.extraConfig = "nohook resolv.conf";
# security.sudo-rs.enable = true; # security.sudo-rs.enable = true;
programs.nix-ld.enable = true; programs.nix-ld.enable = true;
+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;
}; };
} }
+48 -59
View File
@@ -23,62 +23,44 @@ let
in in
{ {
flake.modules = { flake.modules = {
nixos.zsh = { pkgs, ... }: { nixos.zsh = { pkgs, ... }:
users.users."${username}".shell = pkgs.zsh; let
selfPackages = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
in
{
users.users."${username}".shell = selfPackages.jsl-zsh;
programs.zsh.enable = true; programs.zsh.enable = true;
# Already being imported by the john.nix module
# home-manager.sharedModules = [
# inputs.self.modules.homeManager.zsh
# ];
}; };
homeManager.zsh = { pkgs, config, ... }: { homeManager.zsh = { pkgs, config, ... }:
let
selfPackages = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
in
{
programs.zsh = { programs.zsh = {
enable = true; enable = true;
package = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.jsl-zsh; package = selfPackages.jsl-zsh;
enableCompletion = true;
autosuggestion.enable = true;
# syntaxHighlighting.enable = true;
initContent = ''
HOST=$(hostname -s)
${homeEndKeyBindings}
'';
dotDir = "${config.xdg.configHome}/zsh"; 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;
# theme = "risto";
theme = "agnoster";
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; [
fzf
zoxide
];
extraToolPackages = with pkgs; [
lazygit
lazydocker
devenv
self'.packages.shell-tools
self'.packages.neovim-min
];
aliasStr = lib.concatStringsSep "\n" ( aliasStr = lib.concatStringsSep "\n" (
lib.mapAttrsToList (k: v: "alias -- ${lib.escapeShellArg k}=${lib.escapeShellArg v}") { lib.mapAttrsToList (k: v: "alias -- ${lib.escapeShellArg k}=${lib.escapeShellArg v}") {
ls = "eza"; ls = "eza";
@@ -89,10 +71,13 @@ in
ds = "gdu -i /snap /"; ds = "gdu -i /snap /";
ld = "lazydocker"; ld = "lazydocker";
}); });
mkJslZsh = { binName, starshipPackage }:
let
loginBootstrapPath = lib.makeBinPath (integrationPackages ++ [ starshipPackage ] ++ extraToolPackages);
in in
(inputs.wrappers.wrapperModules.zsh.apply { (inputs.wrappers.wrapperModules.zsh.apply {
inherit pkgs; inherit pkgs binName;
binName = "jsl-zsh";
env = { env = {
LANG = "en_US.UTF-8"; LANG = "en_US.UTF-8";
COLORTERM = "truecolor"; COLORTERM = "truecolor";
@@ -116,18 +101,17 @@ in
ignoreAllDups = true; ignoreAllDups = true;
ignoreSpace = true; ignoreSpace = true;
}; };
integrations = {
fzf.enable = true;
starship = {
enable = true;
package = self'.packages.starship;
};
zoxide.enable = true;
};
}; };
extraRC = '' extraRC = ''
${homeEndKeyBindings} ${homeEndKeyBindings}
# Login shells may reset PATH before integrations run.
export PATH=${lib.escapeShellArg loginBootstrapPath}:$PATH
source <(fzf --zsh)
eval "$(zoxide init zsh)"
eval "$(starship init zsh)"
HISTFILE=$HOME/.config/zsh/.zsh_history HISTFILE=$HOME/.config/zsh/.zsh_history
SAVEHIST=${toString historySize} SAVEHIST=${toString historySize}
HISTORY_IGNORE=${lib.escapeShellArg "(${lib.concatStringsSep "|" ignorePatterns})"} HISTORY_IGNORE=${lib.escapeShellArg "(${lib.concatStringsSep "|" ignorePatterns})"}
@@ -137,13 +121,18 @@ in
eval "$(devenv hook zsh)" eval "$(devenv hook zsh)"
''; '';
extraPackages = with pkgs; [ extraPackages = extraToolPackages;
lazygit
lazydocker
devenv
self'.packages.shell-tools
self'.packages.neovim-min
];
}).wrapper; }).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";
}; };
}; };
}; };
+175 -10
View File
@@ -1,25 +1,190 @@
{ inputs, ... }: { inputs, ... }:
{
flake.modules.nixos.step-ca = { config, pkgs, lib, ... }:
let let
ipAddress = "0.0.0.0"; cfg = config.step-ca;
caAddress = "0.0.0.0";
caPort = 443;
caPasswordPath = (lib.getAttr cfg.secrets.caPassword config.sops.secrets).path;
intermediateKeyPath = (lib.getAttr cfg.secrets.intermediateKey config.sops.secrets).path;
sshHostCaKeyPath = (lib.getAttr cfg.secrets.sshHostCaKey config.sops.secrets).path;
sshUserCaKeyPath = (lib.getAttr cfg.secrets.sshUserCaKey config.sops.secrets).path;
adminProvisionerEncryptedKeyValue =
(lib.getAttr cfg.secrets.adminProvisionerEncryptedKey config.sops.placeholder);
renderedStepCaConfig = builtins.toJSON {
root = cfg.rootCertPath;
crt = cfg.intermediateCertPath;
key = intermediateKeyPath;
address = "${caAddress}:${toString caPort}";
dnsNames = cfg.dnsNames;
ssh = {
hostKey = sshHostCaKeyPath;
userKey = sshUserCaKeyPath;
};
db = {
type = "badgerv2";
dataSource = "/var/lib/step-ca/db";
};
authority = {
backdate = "1m0s";
provisioners = [
{
type = "ACME";
name = "acme";
}
{
type = "SSHPOP";
name = "sshpop";
claims.enableSSHCA = true;
}
{
type = "JWK";
name = "admin";
key = {
use = "sig";
kty = "EC";
kid = "xoxgOJFbveSLIL2gm1Yu5ZiRb9v8Jxe44F56i3v-Nf8";
crv = "P-256";
alg = "ES256";
x = "zFO8hPx_eH0Iyz7UJI-w8ODMusEKCZ28M76sGWmWYxA";
y = "XIWLLyKDzqxV9UH-2KeAkKPDrgLoPrxxW9-PzkXggME";
};
encryptedKey = adminProvisionerEncryptedKeyValue;
claims = {
enableSSHCA = true;
disableRenewal = false;
allowRenewalAfterExpiry = false;
disableSmallstepExtensions = false;
};
options = {
x509 = { };
ssh = { };
};
}
];
};
tls = {
cipherSuites = [
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
];
minVersion = 1.2;
maxVersion = 1.3;
renegotiation = false;
};
};
in in
{ {
flake.modules.nixos.step-ca = { pkgs, ... }: { options.step-ca = {
rootCertPath = lib.mkOption {
description = "Path to the Step CA root certificate served by this host.";
type = lib.types.path;
};
intermediateCertPath = lib.mkOption {
description = "Path to the Step CA intermediate certificate served by this host. This is public material and does not need to be stored in SOPS.";
type = lib.types.path;
};
dnsNames = lib.mkOption {
description = "DNS names and IP SANs advertised by this Step CA instance.";
type = with lib.types; listOf str;
};
secrets = {
sopsFile = lib.mkOption {
description = "Host-local SOPS file that stores Step CA secret material.";
type = lib.types.path;
};
caPassword = lib.mkOption {
description = "SOPS key for the Step CA intermediate password.";
type = lib.types.str;
};
intermediateKey = lib.mkOption {
description = "SOPS key for the Step CA intermediate private key.";
type = lib.types.str;
};
sshHostCaKey = lib.mkOption {
description = "SOPS key for the Step SSH host CA private key.";
type = lib.types.str;
};
sshUserCaKey = lib.mkOption {
description = "SOPS key for the Step SSH user CA private key.";
type = lib.types.str;
};
adminProvisionerEncryptedKey = lib.mkOption {
description = "SOPS key for the Step CA admin provisioner encrypted key.";
type = lib.types.str;
};
};
};
config = {
# Placeholders are expected initially until real material is inserted into sops.
sops.secrets."${cfg.secrets.caPassword}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.intermediateKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.sshHostCaKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.sshUserCaKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.secrets."${cfg.secrets.adminProvisionerEncryptedKey}" = {
sopsFile = cfg.secrets.sopsFile;
owner = "step-ca";
group = "step-ca";
mode = "0400";
restartUnits = [ "step-ca.service" ];
};
sops.templates."step-ca-config" = {
owner = "step-ca";
group = "step-ca";
mode = "0400";
content = renderedStepCaConfig;
};
# https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/security/step-ca.nix # https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/security/step-ca.nix
services.step-ca = { services.step-ca = {
enable = true; enable = true;
openFirewall = true; openFirewall = true;
address = ipAddress; address = caAddress;
port = 8443; port = caPort;
intermediatePasswordFile = caPasswordPath;
};
environment.etc."smallstep/ca.json".source =
lib.mkForce config.sops.templates."step-ca-config".path;
systemd.services.step-ca.restartTriggers =
lib.mkAfter [ config.sops.templates."step-ca-config".path ];
# https://smallstep.com/docs/step-ca/configuration/#configuration-options
settings = {
root = "";
crt = "";
};
};
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
step-ca step-ca
step-cli step-cli
]; ];
}; };
};
} }
+28 -16
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}";
selfPkgs = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
in {
home.stateVersion = "25.11"; home.stateVersion = "25.11";
imports = [ imports = with inputs.self.modules.homeManager; [
inputs.self.modules.homeManager.shell-tools ssh-new
inputs.self.modules.homeManager.ssh shell-tools
inputs.self.modules.homeManager.git git
]; ];
# home.packages = [
# inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.shell-tools
# ];
xdg.enable = true; xdg.enable = true;
programs.git.settings.user.name = name; home.packages = [
programs.git.settings.user.email = email; selfPkgs.neovim-min
];
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";
}; };
}; };
} }