Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
19 KiB
AZ-TC-01 AD and WiFi Bootstrap Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Enable Active Directory on AZ-TC-01 and connect the pilot to the temporary WPA-Personal network Pluto without requiring the deferred Saturn EAP-TLS certificate.
Architecture: Keep the host in staging and opt into AD and WiFi only. Model WiFi authentication as an explicit psk/eap-tls mode: PSK mode decrypts a per-host password into /run/agenix and creates a root-only NetworkManager keyfile under /run, while EAP-TLS retains the existing certificate path. Generate the binary AD keytab outside Nix, encrypt it directly with agenix, and validate it before deployment.
Tech Stack: NixOS modules, Nix flakes, agenix/age, NetworkManager/nmcli, systemd, SSSD, MIT Kerberos, adcli.
File map
roles/thin-client/network/wifi.nix— defines WiFi authentication modes and their mode-specific runtime configuration.hosts/AZ-TC-01/default.nix— opts the pilot into AD and PSK WiFi and records confirmed infrastructure values.secrets.nix— grants the host and administrators access to the new PSK secret.secrets/AZ-TC-01-wifi-psk.age— encryptedPlutopassword; created interactively and safe to commit.secrets/AZ-TC-01-krb5-keytab.age— encrypted binary AD machine keytab; replaces the invalid current content.roles/thin-client/README.md— records the PSK bootstrap, correct realm/DCs, binary-keytab handling, and runtime checks.
Execution safety prerequisite
The current working tree already contains broad, uncommitted provisioning work, including modifications to files in this plan. Do not reset, stash, or overwrite it. Before Task 1, either land that existing work in its own reviewed commits or create an exact checkpoint commit agreed with the owner. Then execute this plan on a dedicated branch/worktree. Every commit below must stage only the listed files and must be inspected with git diff --cached before committing.
Task 1: Configure the confirmed AD infrastructure on AZ-TC-01
Files:
-
Modify:
hosts/AZ-TC-01/default.nix:11-24 -
Step 1: Verify the current host does not enable AD
Run:
nix eval --json path:.#nixosConfigurations.AZ-TC-01.config.az.tc.features
Expected before implementation: JSON contains "ad":false and "wifi":false.
- Step 2: Add the pilot feature flags and confirmed AD values
Change the az.tc block in hosts/AZ-TC-01/default.nix to:
az.tc = {
enable = true;
hardwareClass = "generic-x86_64-uefi";
site = "staging";
features = {
ad = true;
wifi = false;
};
ad = {
domain = "az-group.local";
realm = "AZ-GROUP.LOCAL";
ou = "CN=Computers,DC=az-group,DC=local";
domainControllers = [
{
host = "azdc01.az-group.local";
address = "192.168.152.253";
}
{
host = "adpdc01.az-group.local";
address = "192.168.152.254";
}
];
};
};
Leave networking.hostName, az.tc.deployment.diskDevice, and system.stateVersion unchanged.
- Step 3: Evaluate the AD settings
Run:
nix eval --raw path:.#nixosConfigurations.AZ-TC-01.config.az.tc.ad.realm
nix eval --json path:.#nixosConfigurations.AZ-TC-01.config.az.tc.ad.domainControllers
nix eval --json path:.#nixosConfigurations.AZ-TC-01.config.age.secrets
Expected:
-
First command prints
AZ-GROUP.LOCAL. -
DC JSON contains both confirmed host/IP pairs.
-
Required secrets contain
AZ-TC-01-krb5-keytabbut no WiFi certificate or PSK yet. -
Step 4: Commit the host AD configuration
git add hosts/AZ-TC-01/default.nix
git diff --cached
git commit -m "feat(thin-client): configure AZ-TC-01 Active Directory"
Task 2: Add an explicit PSK WiFi mode
Files:
-
Modify:
roles/thin-client/network/wifi.nix:1-122 -
Step 1: Write the failing evaluation checks
Run these against the current module:
nix eval --raw path:.#nixosConfigurations.AZ-TC-01.options.az.tc.wifi.mode.type.name
nix eval --raw path:.#nixosConfigurations.AZ-TC-01.config.systemd.services.wifi-psk-provision.description
Expected before implementation: both fail because az.tc.wifi.mode and wifi-psk-provision do not exist.
- Step 2: Add the mode option
In options.az.tc.wifi, before ssid, add:
mode = mkOption {
type = types.enum ["psk" "eap-tls"];
default = "eap-tls";
description = "WiFi authentication mode: temporary WPA-Personal PSK or machine EAP-TLS.";
};
Keeping eap-tls as the default preserves existing behavior for other hosts.
- Step 3: Split shared, EAP-TLS, and PSK configuration
Add mkMerge to the inherited lib functions:
inherit (lib) mkIf mkMerge mkOption types;
Replace the current config = mkIf ... { ... }; body with this shape, moving the existing EAP-TLS profile and certificate secret unchanged into the first mode-specific block:
config = mkIf (cfg.enable && cfg.features.wifi) (mkMerge [
{
networking.wireless.iwd.enable = false;
networking.networkmanager = {
enable = true;
wifi.backend = "wpa_supplicant";
};
systemd.tmpfiles.rules = [
"d /etc/wifi 0700 root root -"
"d /run/NetworkManager/system-connections 0700 root root -"
];
systemd.services.NetworkManager = {
after = ["age-identity.service"];
wants = ["age-identity.service"];
};
}
(mkIf (cfg.wifi.mode == "eap-tls") {
environment.etc."NetworkManager/system-connections/${cfg.wifi.ssid}.nmconnection" = {
mode = "0600";
source = pkgs.writeText "${cfg.wifi.ssid}.nmconnection" ''
[connection]
id=${cfg.wifi.ssid}
type=wifi
autoconnect=true
permissions=
[wifi]
mode=infrastructure
ssid=${cfg.wifi.ssid}
[wifi-security]
key-mgmt=wpa-eap
[802-1x]
eap=tls
identity=${hostname}$
ca-cert=${cfg.wifi.caCert}
client-cert=/etc/wifi/client.pem
private-key=/etc/wifi/client.pem
private-key-password=
phase2-auth=
[ipv4]
method=auto
[ipv6]
method=auto
'';
};
age.secrets."${hostname}-wifi-client-cert" = {
file = ../../../secrets/${hostname}-wifi-client-cert.age;
path = "/etc/wifi/client.pem";
mode = "0600";
owner = "root";
group = "root";
};
})
(mkIf (cfg.wifi.mode == "psk") {
age.secrets."${hostname}-wifi-psk" = {
file = ../../../secrets/${hostname}-wifi-psk.age;
mode = "0400";
owner = "root";
group = "root";
};
systemd.services.wifi-psk-provision = {
description = "Provision the ${cfg.wifi.ssid} WPA-Personal connection";
wantedBy = ["multi-user.target"];
after = ["NetworkManager.service" "age-identity.service"];
wants = ["NetworkManager.service" "age-identity.service"];
path = [pkgs.coreutils pkgs.networkmanager];
environment = {
WIFI_CONNECTION = "az-tc-${cfg.wifi.ssid}";
WIFI_SSID = cfg.wifi.ssid;
WIFI_PSK_FILE = config.age.secrets."${hostname}-wifi-psk".path;
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -eu
test -s "$WIFI_PSK_FILE"
install -d -m 0700 /run/NetworkManager/system-connections
profile="/run/NetworkManager/system-connections/$WIFI_CONNECTION.nmconnection"
umask 077
{
printf '%s\n' \
'[connection]' \
"id=$WIFI_CONNECTION" \
'type=wifi' \
'autoconnect=true' \
'' \
'[wifi]' \
'mode=infrastructure' \
"ssid=$WIFI_SSID" \
'' \
'[wifi-security]' \
'key-mgmt=wpa-psk'
printf 'psk='
cat "$WIFI_PSK_FILE"
printf '%s\n' \
'' \
'[ipv4]' \
'method=auto' \
'' \
'[ipv6]' \
'method=auto'
} > "$profile"
chmod 0600 "$profile"
nmcli connection reload
nmcli connection up id "$WIFI_CONNECTION"
'';
};
})
]);
The keyfile lives under /run, is root-only, and never enters the Nix store. The PSK is read from the agenix runtime file and is never passed as a command-line argument.
- Step 4: Format and evaluate the module
Run:
nix fmt roles/thin-client/network/wifi.nix
nix eval --raw path:.#nixosConfigurations.AZ-TC-01.options.az.tc.wifi.mode.type.name
Expected: formatting succeeds and the option evaluates as an enum type. The service still does not exist because the host has not selected PSK mode.
- Step 5: Commit the mode implementation
git add roles/thin-client/network/wifi.nix
git diff --cached --check
git diff --cached
git commit -m "feat(thin-client): support WPA-PSK WiFi bootstrap"
Task 3: Select Pluto and register its secret
Files:
-
Modify:
hosts/AZ-TC-01/default.nix:11-42 -
Modify:
secrets.nix:31-53 -
Step 1: Enable PSK WiFi for the pilot
In hosts/AZ-TC-01/default.nix, change the feature flag and add the WiFi settings inside az.tc:
features = {
ad = true;
wifi = true;
};
wifi = {
mode = "psk";
ssid = "Pluto";
};
- Step 2: Register the encrypted PSK file
In secrets.nix, update the per-host secret documentation to include:
# - <host>-wifi-psk.age — temporary WPA-Personal password
Add this AZ-TC-01 recipient rule next to its other per-host secrets:
"secrets/AZ-TC-01-wifi-psk.age".publicKeys = [AZ-TC-01] ++ users;
- Step 3: Verify mode-specific secret selection
Run:
nix eval --json path:.#nixosConfigurations.AZ-TC-01.config.age.secrets \
| jq -r 'keys[]' \
| sort
Expected output contains exactly these feature-specific entries:
AZ-TC-01-krb5-keytab
AZ-TC-01-wifi-psk
It must not contain AZ-TC-01-wifi-client-cert, NetBird, RustDesk, or monitoring secrets.
Run:
nix eval --raw path:.#nixosConfigurations.AZ-TC-01.config.systemd.services.wifi-psk-provision.description
Expected: Provision the Pluto WPA-Personal connection.
- Step 4: Commit host selection and recipient rule
git add hosts/AZ-TC-01/default.nix secrets.nix
git diff --cached --check
git diff --cached
git commit -m "feat(thin-client): bootstrap AZ-TC-01 on Pluto"
Task 4: Create and validate the Pluto PSK secret
Files:
-
Create:
secrets/AZ-TC-01-wifi-psk.age -
Step 1: Enter the password locally without putting it in shell history or chat
Run from the repository root in an interactive terminal:
read -rsp 'Pluto WiFi password: ' WIFI_PSK
printf '\n'
printf '%s' "$WIFI_PSK" | nix develop --command agenix -e secrets/AZ-TC-01-wifi-psk.age
unset WIFI_PSK
Expected: agenix creates a non-empty encrypted file. Do not use echo, because it may add an unintended newline.
- Step 2: Verify the host identity can decrypt the secret without printing it
nix shell nixpkgs#age -c age --decrypt \
-i .secrets/identities/AZ-TC-01.age \
secrets/AZ-TC-01-wifi-psk.age \
| test -s /dev/stdin
Expected: exit status 0 and no secret output.
- Step 3: Commit only the encrypted file
git add secrets/AZ-TC-01-wifi-psk.age
git diff --cached --stat
git commit -m "chore(secrets): add AZ-TC-01 Pluto credential"
Task 5: Generate and replace the AD machine keytab
Files:
-
Modify:
secrets/AZ-TC-01-krb5-keytab.age -
Step 1: Check AD discovery and clock before joining
On an admin workstation connected to the internal network, run:
nix shell nixpkgs#adcli nixpkgs#krb5 nixpkgs#bind -c bash
dig +short SRV _kerberos._tcp.az-group.local @192.168.152.253
dig +short SRV _ldap._tcp.dc._msdcs.az-group.local @192.168.152.253
timedatectl status
Expected: both azdc01.az-group.local and adpdc01.az-group.local appear, and system time synchronization is active.
- Step 2: Obtain an administrator Kerberos ticket
kinit administrator@AZ-GROUP.LOCAL
klist
Expected: a valid TGT for administrator@AZ-GROUP.LOCAL. If the authorized join account has another name, substitute it consistently.
- Step 3: Create the default-container computer account and binary keytab
Because no ThinClients OU exists, omit --domain-ou and let AD use CN=Computers:
umask 077
adcli join \
--domain=az-group.local \
--domain-controller=azdc01.az-group.local \
--host-fqdn=AZ-TC-01.az-group.local \
--computer-name=AZ-TC-01 \
--os-name=NixOS \
--os-version=26.05 \
--login-ccache="${KRB5CCNAME:-/tmp/krb5cc_$(id -u)}" \
--host-keytab=/tmp/AZ-TC-01.keytab \
--verbose
If the installed adcli exposes only the short option for the keytab path, replace --host-keytab=/tmp/AZ-TC-01.keytab with -K /tmp/AZ-TC-01.keytab after confirming via adcli join --help.
- Step 4: Validate the binary keytab before encryption
klist -k -e /tmp/AZ-TC-01.keytab
Expected: principals for the machine and FQDN in AZ-GROUP.LOCAL, including AZ-TC-01$@AZ-GROUP.LOCAL and host/AZ-TC-01.az-group.local@AZ-GROUP.LOCAL.
- Step 5: Encrypt the binary file directly and validate the round trip
nix develop --command agenix -e secrets/AZ-TC-01-krb5-keytab.age \
< /tmp/AZ-TC-01.keytab
verified_keytab="$(mktemp)"
chmod 0600 "$verified_keytab"
trap 'rm -f "$verified_keytab" /tmp/AZ-TC-01.keytab' EXIT
nix shell nixpkgs#age -c age --decrypt \
-i .secrets/identities/AZ-TC-01.age \
secrets/AZ-TC-01-krb5-keytab.age \
> "$verified_keytab"
nix shell nixpkgs#krb5 -c klist -k -e "$verified_keytab"
Expected: the decrypted copy has the same valid principals. Never paste the keytab into an editor and never print its binary contents.
- Step 6: Remove plaintext and commit only the encrypted replacement
rm -f "$verified_keytab" /tmp/AZ-TC-01.keytab
trap - EXIT
git add secrets/AZ-TC-01-krb5-keytab.age
git diff --cached --stat
git commit -m "chore(secrets): provision AZ-TC-01 AD keytab"
Task 6: Update the operator runbook
Files:
-
Modify:
roles/thin-client/README.md:21-25,39-180,186-194 -
Step 1: Correct the pilot infrastructure table and workflow
Document these exact facts:
- AD DNS domain: `az-group.local`
- Kerberos realm: `AZ-GROUP.LOCAL`
- DCs: `azdc01.az-group.local` (`192.168.152.253`) and `adpdc01.az-group.local` (`192.168.152.254`)
- Computer objects currently use the default `CN=Computers` container.
- Pilot WiFi uses WPA-Personal SSID `Pluto`; `Saturn` EAP-TLS is deferred.
Replace any instruction to paste a keytab into an editor with the binary-safe command:
agenix -e secrets/AZ-TC-01-krb5-keytab.age < /tmp/AZ-TC-01.keytab
Add the local PSK creation and no-output decryption check from Task 4. State explicitly that the password must not be committed in plaintext or sent through chat.
- Step 2: Add post-boot AD checks
Add this operator checklist:
nmcli --fields NAME,TYPE,DEVICE connection show --active
resolvectl query azdc01.az-group.local adpdc01.az-group.local
systemctl --no-pager --full status sssd
sssctl domain-status az-group.local
getent passwd '<known-ad-user>'
id '<known-ad-user>'
Explain that <known-ad-user> is replaced with a real non-administrator test account and that offline login is tested only after one successful online login.
- Step 3: Format/check and commit the runbook
git diff --check -- roles/thin-client/README.md
git add roles/thin-client/README.md
git diff --cached
git commit -m "docs(thin-client): document AD and Pluto bootstrap"
Task 7: Run static and build verification
Files:
-
No new files.
-
Step 1: Check formatting and module evaluation
nix fmt -- --check .
nix flake check
Expected: both commands exit 0.
- Step 2: Confirm only the intended secrets are required
nix eval --json path:.#nixosConfigurations.AZ-TC-01.config.age.secrets \
| jq -r 'keys[]' \
| sort
Expected feature-specific keys:
AZ-TC-01-krb5-keytab
AZ-TC-01-wifi-psk
- Step 3: Run local and remote deployment preflight
Boot the target from a supported live ISO, enable root SSH, identify its IP, and run:
nix run .#deploy -- --check AZ-TC-01 root@<target-ip>
Expected: evaluation, secret decryption, SSH, and target disk checks pass without modifying the target.
- Step 4: Build the host closure
nix build --no-link .#nixosConfigurations.AZ-TC-01.config.system.build.toplevel
Expected: exit 0.
- Step 5: Review the implementation commits
git status --short
git log --oneline --decorate -8
git diff origin/master...HEAD -- \
hosts/AZ-TC-01/default.nix \
roles/thin-client/network/wifi.nix \
roles/thin-client/README.md \
secrets.nix
Expected: no plaintext credential appears, no Saturn certificate is required, and unrelated pre-existing changes are not part of these commits.
Task 8: Deploy and verify the pilot
Files:
-
No new files.
-
Step 1: Confirm the destructive disk target
On the live target:
lsblk -o NAME,PATH,SIZE,TYPE,MODEL,SERIAL,MOUNTPOINTS
Expected: the intended disposable installation disk exactly matches az.tc.deployment.diskDevice. Stop if /dev/nvme0n1 is not the intended disk.
- Step 2: Install through the confirmation-protected wrapper
nix run .#deploy -- AZ-TC-01 root@<target-ip>
Expected: the wrapper displays hardware and requires typing AZ-TC-01 before erasing the disk.
- Step 3: Verify WiFi, DNS, and SSSD after reboot
nmcli --fields NAME,TYPE,DEVICE connection show --active
resolvectl query azdc01.az-group.local adpdc01.az-group.local
sudo klist -k -e /etc/krb5.keytab
systemctl --no-pager --full status sssd wifi-psk-provision
sssctl domain-status az-group.local
Expected: az-tc-Pluto is active, both DC names resolve, the keytab contains AZ-GROUP.LOCAL principals, and both services are healthy.
- Step 4: Verify AD identity and login
getent passwd '<known-ad-user>'
id '<known-ad-user>'
Expected: both commands resolve the same real non-administrator AD account. Then log in once through SDDM while online, disconnect the network, and verify that the same account can log in from SSSD's credential cache.
- Step 5: Record any environment-specific failure as a follow-up issue
If DNS, keytab principals, GPO access, or WPA compatibility differs from the validated assumptions, capture the exact command output and open a focused bd issue. Do not weaken TLS, expose the PSK, or disable SSSD validation as a workaround.