Config of my NixOS hosts
- Nix 38.3%
- Shell 31.5%
- JavaScript 8.1%
- Python 6.3%
- QML 6.2%
- Other 9.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docs | ||
| hosts | ||
| lib | ||
| modules | ||
| packages | ||
| secrets | ||
| ssh_keys | ||
| tests | ||
| .envrc | ||
| .gitignore | ||
| .sops.yaml | ||
| common.nix | ||
| flake.lock | ||
| flake.nix | ||
| overlay.nix | ||
| README.adoc | ||
| UNLICENSE | ||
== Manage Stellaris with FOS
`fos` is the Froidmont Operating System command center. FOS v2 uses grouped
commands such as `fos nixos build`; this is the new command structure, and the
old flat FOS v1 commands have no compatibility aliases. Run `fos` without
arguments for the full overview, or use the discoverability commands:
[source,bash]
----
fos help
fos help network wifi
fos help nixos build
fos commands
fos commands --json
fos status
fos doctor
----
`fos help [COMMAND...]` shows either all usage or the matching command subtree.
`fos commands` lists the command registry, while `fos commands --json` exposes
that registry to scripts. `fos status` safely summarizes the host, kernel,
uptime, load, memory, network, power profile, and FOS recording state.
`fos doctor` checks important command backends and graphical-session state;
both status and doctor also accept `--json`.
The same registry drives the installed Zsh autocomplete. In addition to command
paths and options, completion discovers current NixOS hosts, media players,
Wi-Fi networks and interfaces, Bluetooth devices, VPN servers, system and user
services, virtual machines, and disks.
=== Desktop command menu
The Quickshell command menu provides a searchable, hierarchical desktop view
of curated FOS actions. `Super+Space` toggles the root menu,
`Super+Alt+Space` opens installed applications, and `Super+Escape` opens the
System submenu.
The menu covers desktop controls, capture, network and VPN actions, NixOS
workflows, hardware information, common tools, and session or power actions.
Commands that need arbitrary arguments remain in the CLI. Mutating and
disruptive menu actions show a confirmation dialog that defaults to Cancel;
actions normally guarded by FOS receive `--yes` only after confirmation.
[source,bash]
----
fos menu
fos menu apps
fos menu system
----
`fos` is the supported unified desktop-management interface. Quickshell and FOS
share private `fos-internal-*` helpers that are not installed in the interactive
user `PATH`. Existing application-role launchers remain available for desktop
integration. Upstream Omarchy source paths, configuration paths, and
`omarchy.*` plugin IDs remain implementation details of the imported shell.
=== Work proxy
When `modules.services.work-proxy.enable` is enabled, the configured Firefox
automatically loads Foyer Impersonation 2.0.2 from a hash-pinned local archive at
each browser startup. The unsigned add-on uses Firefox's temporary-install API;
there is no manual loading step after restarting, and normal extension-signature
checks remain enabled. Existing installations of the same add-on are left alone,
and the loader is skipped in Troubleshoot Mode.
The loader requires privileged AutoConfig JavaScript, not a different Firefox
edition or a disabled content-process sandbox. This uses internal Firefox APIs,
so verify `about:debugging#/runtime/this-firefox` after Firefox upgrades; startup
errors are logged in the Browser Console with the `[work-proxy-firefox]` prefix.
Restored tabs may need reloading if they make requests before the add-on starts.
=== NixOS lifecycle
FOS uses the configured NixOS checkout from `programs.nh.flake` and can be run
from any directory.
[source,bash]
----
fos nixos hosts
fos nixos build
fos nixos build aegis
fos nixos test
fos nixos switch
fos nixos boot
fos nixos update
fos nixos update nixpkgs home-manager
fos nixos generations
fos nixos rollback
fos nixos rollback 42
----
Only `fos nixos build` accepts another host. Activation and rollback commands
always target the local machine. `fos nixos update` updates the selected inputs
and builds the local configuration without activating it.
`fos nixos` is intentionally the boundary for the Nix lifecycle. FOS does not
mutate Nix-managed theme, font, package, default, or application configuration.
Docker management is explicitly out of scope for now.
=== Command domains
The grouped domains cover the common operational tasks without replacing
declarative Nix configuration:
* `nixos` builds, activates, updates, and rolls back host configurations;
`auth` inspects and refreshes GitLab credentials used by Nix.
* `menu` opens desktop controls and `launch` starts standard applications.
* `audio` manages volume, mute, devices, and the mixer; `media` controls active
players; `dictation` controls local speech-to-text; `display` reports and
adjusts brightness, scale, and layout.
* `network` reports connectivity and controls Wi-Fi; `bluetooth` manages its
radio and devices.
* `power` covers batteries and profiles; `system` covers statistics, locking,
session actions, and power actions.
* `notifications` and `clipboard` expose desktop history and controls.
* `capture` provides screenshots, OCR, and screen or region recording.
* `vpn` controls the Aegis VPN gateway, while `tailscale` controls Tailscale.
* `service` inspects, logs, and restarts system or user units; `vm` manages
libvirt virtual machines; `hardware` provides read-only hardware and disk
information.
Disruptive operations ask for confirmation. Pass `--yes` to explicitly approve
them, especially in non-interactive use. Wi-Fi QR sharing reveals credentials
and therefore requires `--reveal` (for example,
`fos network wifi share --reveal`).
=== Screenshot behavior
`Print` freezes the visible workspace and opens a smart picker. Drag for a
freeform region, or click a window or monitor to capture its rectangle. While
the picker is open, `Return` captures the highlighted window, `Ctrl+Return`
captures the focused monitor, `Tab` and `Ctrl+Tab` cycle through windows, and
the arrow keys move between windows. Press `Print` again to cancel.
Screenshots are written to `~/Pictures/Screenshots` and copied immediately to
the clipboard as PNG images. The completion notification contains a preview;
click it or press `Super+Alt+N` to annotate the saved image in Satty.
Clipboard history can also edit any stored image with `Ctrl+Enter` or a
right-click; the edited result is saved and added as a new clipboard entry.
`Shift+Print` captures the focused monitor directly, `Alt+Print` opens recording
controls, `Super+Print` picks a color, `Super+Ctrl+Print` copies OCR text from a
region, and `Super+Ctrl+C` opens the Capture menu.
The explicit `fos capture screenshot smart`, `region`, `window`, and `screen`
commands provide the same capture modes. Set `FOS_SCREENSHOT_DIR` to override
the screenshot directory for an individual invocation or graphical session.
=== Recording behavior
`fos capture record start region` and `fos capture record start screen` launch
`wf-recorder`; add `--audio` to include audio. FOS prints the output path and
writes uniquely named, timestamped MP4 files below `$XDG_VIDEOS_DIR`, or
`~/Videos` when it is unset. It tracks the recorder identity and output path in
`$XDG_RUNTIME_DIR/fos/recording.state`, falling back to a private
`$TMPDIR/fos-$UID` directory when necessary, so `fos capture record status` and
`stop` can validate the active process.
`fos capture record stop` only signals the recorder started and owned by FOS;
it refuses stale or unowned state rather than stopping another recorder.
=== Dictation behavior
Press `Super+X` to start recording and press it again to transcribe into the
focused application. The same operations are available through `fos dictation
start`, `stop`, `toggle`, and
`status`, and the status bar shows recording and transcription activity.
Voxtype performs transcription locally with the multilingual Whisper base model
and automatic language detection. Its configuration and model are managed by
Nix; typed output falls back to the clipboard when direct Wayland input is not
available.
== Bootstrap or manage without FOS
[source,bash]
----
sudo nixos-rebuild switch --flake .#thehostname
----
If `#thehostname` is omitted, `nixos-rebuild` defaults to the current hostname.
Build another host without activation with:
[source,bash]
----
nix build .#nixosConfigurations.thehostname.config.system.build.toplevel
----
Update the lock file without FOS with `nix flake update`.
== Build and flash the Voyager firmware
The Voyager AZERTY keymap source is kept in `packages/voyager-firmware` and
built against the ZSA `firmware25` QMK fork pinned in `flake.lock`.
[source,bash]
----
nix build .#voyager-firmware
----
The firmware is available at `result/zsa_voyager_azerty.bin`. Build and flash
it from the command line with:
[source,bash]
----
nix run .#voyager-flash
----
Zapp waits for the keyboard to enter bootloader mode. Use the Voyager's reset
button or the `QK_BOOT` key in the layout after starting the command. Update the
pinned ZSA firmware separately, then compile before flashing it:
[source,bash]
----
nix flake update qmk-firmware
nix build .#voyager-firmware
----
== Refresh Nix GitLab authentication
[source,bash]
----
fos auth refresh
fos auth refresh gitlab.example.com
----
The GitLab host defaults to `gitlab.com`. Authenticate with `glab auth login`
before refreshing a missing token.
== Build the Aegis installer ISO
[source,bash]
----
nix build .#nixosConfigurations.aegis-installer.config.system.build.isoImage
----
The built image is available under `result/iso/`.
== Write installer ISO to USB
[source,bash]
----
sudo dd if=result/iso/*.iso of=/dev/sdX bs=4M conv=fsync status=progress
----
Replace `/dev/sdX` with your USB device.
== Install Aegis from the custom ISO
Boot the Protectli from the USB stick, then run:
[source,bash]
----
install-aegis
----
The installer wipes `/dev/nvme0n1`, creates EFI + ext4 partitions, and installs `.#aegis`.
You can target another disk with `install-aegis /dev/your-disk`.
After first boot, connect with SSH as user `admin` using your configured SSH key.
If Newt is enabled, read the SOPS host-identity recovery requirements in
<<_aegis_pangolin_management,Aegis Pangolin management>> before reinstalling.
== Aegis DNS and DHCP
- `aegis` provides DNS filtering with AdGuard Home on `192.168.1.1:53`.
- The AdGuard web UI is available on LAN at `http://192.168.1.1:3000`.
- `dnsmasq` provides DHCP on LAN (`192.168.1.100-192.168.1.199`).
AdGuard web authentication is required. Create the admin hash file on `aegis`:
[source,bash]
----
PASSWORD='choose-a-strong-password'
HASH="$(nix shell nixpkgs#apacheHttpd -c htpasswd -nbBC 12 "" "$PASSWORD" | tr -d ':\n')"
sudo install -d -m 0755 /etc/secrets
sudo sh -c 'printf "%s\n" "$1" > /etc/secrets/aegis-adguard-admin-password-hash' _ "$HASH"
sudo chmod 0644 /etc/secrets/aegis-adguard-admin-password-hash
----
== Aegis Pangolin management
The configuration enables a Newt 1.16.0 system service against
`https://pangolin.banditlair.com`, using the enrolled `aegis` site's encrypted
credentials.
Credentials are managed by sops-nix, not a manually provisioned file on the
router. `.sops.yaml` permits decryption by Aegis's existing Ed25519 SSH host
identity or the operator's existing GPG recovery key
(`3AC6F170F01133CE393BCD94BE948AFD7E7873BE`). No other host is a recipient.
The project direnv environment provides SOPS and GnuPG from the pinned stable Nixpkgs.
Run `direnv allow` once in this repository to enable it. Edit the existing site's
credentials with:
[source,bash]
----
sops edit secrets/aegis-newt.enc.yml
----
Without direnv, use `nix develop -c sops edit secrets/aegis-newt.enc.yml`.
For a fresh enrollment, create a Newt site named `aegis` in the existing Pangolin
organization. The encrypted file has the following structure in the SOPS editor,
with real credentials from Pangolin instead of placeholders:
[source,yaml]
----
newt:
environment: |
NEWT_ID=<site ID>
NEWT_SECRET=<site secret>
----
Use an editor with persistent swap, backup and undo files disabled. Only SOPS
ciphertext belongs in `secrets/aegis-newt.enc.yml`; never write plaintext into
the repository, command arguments, shell history or Nix expressions. The secrets
directory ignores non-encrypted files, but this is not a substitute for checking
what is staged. Keep the operator's private recovery key outside the repository.
For a fresh enrollment, keep Newt disabled until the encrypted file exists, then
set `services.newt.enable = true` in `hosts/aegis/newt.nix`.
Run `git add secrets/aegis-newt.enc.yml` to include a new encrypted file in the
Git-backed flake source before building. An enabled connector with a missing
encrypted file fails evaluation rather than silently skipping the service.
Deployment decrypts the environment block into root-owned mode `0400`
`/run/secrets/newtAegisEnvironment`; no manual plaintext copy to Aegis is needed.
Secret changes automatically restart `newt.service`.
Build and deploy through the existing LAN connection, retaining rollback and
console access:
[source,bash]
----
nix build --no-link .#nixosConfigurations.aegis.config.system.build.toplevel
nix build --no-link .#checks.x86_64-linux.aegis-newt
NIX_SSHOPTS='-o StrictHostKeyChecking=yes' nixos-rebuild test --flake .#aegis --target-host admin@192.168.1.1 --sudo
ssh -o StrictHostKeyChecking=yes admin@192.168.1.1 'sudo systemctl is-active newt.service; sudo stat -Lc "%U:%G %a %n" /run/secrets/newtAegisEnvironment'
----
Verify access below before persisting with `nixos-rebuild switch` using the same
flake, target and SSH options. Rotate credentials through the same SOPS editor
and deploy again. After changing recipients in `.sops.yaml`, run
`sops updatekeys secrets/aegis-newt.enc.yml`. Removing a recipient does not revoke
its access to historical ciphertext; rotate the actual Newt credential after a
recipient compromise.
A reinstall must preserve the existing SSH host identity or re-encrypt the secret
to the replacement host's verified public key using the operator recovery key.
The disk-wiping Aegis installer does not restore that identity. Keep Newt disabled
during a fresh-identity bootstrap until the new recipient is enrolled; do not put
private host keys in Git or the installer image.
The sole private resource, `aegis-management` (`Aegis management`), targets host
`aegis-target.home.internal`, resolved locally on Aegis to `192.168.1.1`, with
client alias `aegis.home.internal`. Do not replace the hostname destination with
the literal IP: Pangolin would advertise `192.168.1.1/32` to clients, capturing
their normal LAN router traffic. The hostname destination exposes only the
virtual alias. It grants the `Personal` role TCP ports `22,3000` only, with no
individual user grants, UDP or ICMP. It is not a
whole-LAN resource. Pangolin retains its automatic organization-wide `Admin`
access. Newt's built-in SSH is disabled; ordinary OpenSSH and AdGuard authentication
remain required.
Connect an authorized Pangolin client, then use `http://aegis.home.internal:3000`
or SSH as `admin`. First establish and verify the router's SSH host key for
`192.168.1.1` over a trusted LAN connection; require that same identity via the
alias, without accepting a separate alias key:
[source,bash]
----
ssh -o StrictHostKeyChecking=yes -o HostKeyAlias=192.168.1.1 admin@aegis.home.internal
----
The router's hosts entry may also be served by its LAN DNS. This configuration
leaves gateway routing, firewall rules and DNS listeners unchanged and adds no
Mullvad bypass. Before relying on remote access, test SSH and AdGuard with the VPN
down, up, switching servers and in a failed-tunnel/kill-switch state. Keep
direct LAN SSH and local console recovery available: an active but broken Mullvad
tunnel also prevents Newt from connecting until the tunnel recovers or is brought
down locally.
== Aegis Wi-Fi (WPA2)
- SSID: `NSA honeypot`.
- Create the WPA2 passphrase file on `aegis`:
[source,bash]
----
sudo install -d -m 0755 /etc/secrets
sudo install -m 0600 /dev/null /etc/secrets/aegis-wifi-passphrase
sudo sh -c 'printf "%s\n" "your-8-to-63-char-passphrase" > /etc/secrets/aegis-wifi-passphrase'
----
== Aegis Mullvad VPN gateway (kill-switch)
`aegis` can run a Mullvad WireGuard client and route all LAN traffic through it.
Server profiles are generated automatically from Mullvad's public relay list.
The router only needs one registered device identity; it does not store your
Mullvad account number. Account renewal and replacing a revoked device remain
manual operations.
1. On a new installation, obtain one WireGuard configuration for the Aegis device
from Mullvad and install it privately on `aegis`:
[source,bash]
----
sudo install -d -m 0700 /etc/secrets/mullvad
sudo install -o root -g root -m 0600 /path/to/mullvad/device.conf /etc/secrets/mullvad/current.conf
sudo mullvad-gw refresh
----
Existing installations already have `current.conf`; do not overwrite it.
The first refresh migrates its private key and IPv4 address to the root-only
`/etc/secrets/mullvad/identity.conf`. It preserves the old `servers` directory
as `servers.pre-refresh.<timestamp>` and replaces it with a managed symlink.
Treat both the identity and the backup as secrets; never commit them or place
them in the Nix store. After verifying the migration, the old backup may be
removed manually. Do not edit the generated server profiles.
The `mullvad-relay-refresh` timer refreshes all active direct WireGuard servers
after boot and daily. Invalid metadata or a failed download leaves the previous
profile set intact. The selected server is retained while available; otherwise
another server in the same country is selected, falling back to Switzerland.
If neither country has an available server, refresh fails without publishing
the new set. A stopped VPN stays stopped. An active VPN is restarted only when
the selected profile changes, and a failed handshake leaves the kill-switch on.
An unchanged refresh does not retry or replace an unhealthy active server;
use `up` to retry or `switch` to choose another server.
Metadata recovery uses marked WAN sockets for the fixed Mullvad HTTPS API and
DNS queries for that API through `1.1.1.1`, even when the tunnel is active.
TLS certificates are verified and redirects are rejected. This reveals the
metadata lookup/connection to the upstream network but does not bypass the VPN
for LAN traffic or other applications.
2. Control it on `aegis`:
[source,bash]
----
sudo mullvad-gw list
sudo mullvad-gw refresh
sudo mullvad-gw up
sudo mullvad-gw status
sudo mullvad-gw switch nl-ams-wg-001
sudo mullvad-gw down
----
`up` enables a LAN kill-switch (LAN forwarding is blocked unless traffic exits via `mullvad`).
`down` deliberately restores direct internet access. The VPN remains opt-in
after reboot; the refresh timer does not enable it. `status` checks handshake
freshness and traffic counters, not end-to-end DNS or internet reachability.
3. Control it remotely from `stellaris`:
[source,bash]
----
fos vpn list
fos vpn refresh
fos vpn up
fos vpn status
fos vpn switch nl-ams-wg-001
fos vpn down
----
The underlying `aegis-vpn` command remains available for direct use.
`fos vpn up` and `fos vpn down` run without confirmation and do not accept
`--yes`; server switching still requires confirmation. Refresh may reconnect
an active VPN if the selected profile has changed.
Inspect automatic refresh failures on Aegis with:
[source,bash]
----
systemctl status mullvad-relay-refresh.service mullvad-relay-refresh.timer
journalctl -u mullvad-relay-refresh.service
----