Phase 7: Dotfiles & Stow
Phase 7: Secrets Bootstrap & Dotfiles
The P16g has no SSH keys, no GPG keys, no gopass, no secrets. You can’t clone anything from GitHub without bootstrapping credentials first. Everything comes from the Razer via rsync.
Bootstrap Order
| Step | What | Why |
|---|---|---|
1 |
|
SSH keys + known_hosts → can authenticate to GitHub/GitLab/Gitea |
2 |
|
GPG private keys + trust → YubiKey works, gopass can decrypt |
3 |
|
age identities + recipients → can decrypt age-encrypted files (SSH config in dots-quantum) |
4 |
gopass stores |
|
5 |
|
Certs, LUKS headers, email config, environment scripts, shell-security |
6 |
ssh-add |
Load SSH keys with passphrases from gopass |
7 |
git clone |
Now everything works |
Step 1: rsync from Razer
Run these from the Razer (not the P16g). Replace the P16g IP if it changed after reboot.
Prerequisites on P16g
rsync must be installed, and password auth must be temporarily enabled (the Razer’s SSH config blocks password auth by default — the Host * block overrides -o flags).
# On the P16g — install rsync
sudo pacman -S rsync
# On the P16g — temporarily enable password auth for rsync bootstrap
sudo sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
sudo sed -i 's/^KbdInteractiveAuthentication no/KbdInteractiveAuthentication yes/' /etc/ssh/sshd_config.d/99-archlinux.conf
sudo systemctl restart sshd
Create target directories on P16g
rsync can create the final directory but not parent paths. Create them first:
# On the P16g
mkdir -p ~/.local/share/gopass
mkdir -p ~/.config/gopass
mkdir -p ~/.ssh ~/.gnupg ~/.age ~/.secrets
rsync from Razer
-F /dev/null bypasses the Razer’s SSH config (which has a Host * block that forces pubkey-only). Without this flag, rsync won’t prompt for a password.
P16G_IP="10.50.40.166"
# SSH keys (16 keypairs + known_hosts)
rsync -avz --chmod=D700,F600 -e "ssh -F /dev/null" ~/.ssh/ evanusmodestus@${P16G_IP}:~/.ssh/
# GPG keys (private keys, trust db, agent config)
rsync -avz --chmod=D700,F600 -e "ssh -F /dev/null" ~/.gnupg/ evanusmodestus@${P16G_IP}:~/.gnupg/
# age keys (identities + recipients)
rsync -avz --chmod=D700,F600 -e "ssh -F /dev/null" ~/.age/ evanusmodestus@${P16G_IP}:~/.age/
# gopass stores (v2 + v3)
rsync -avz -e "ssh -F /dev/null" ~/.local/share/gopass/ evanusmodestus@${P16G_IP}:~/.local/share/gopass/
# Secrets repository (certs, LUKS headers, email config, scripts)
rsync -avz -e "ssh -F /dev/null" ~/.secrets/ evanusmodestus@${P16G_IP}:~/.secrets/
# gopass configuration (store paths, settings — without this, gopass doesn't find the stores)
rsync -avz -e "ssh -F /dev/null" ~/.config/gopass/ evanusmodestus@${P16G_IP}:~/.config/gopass/
|
If you already locked down sshd after the first rsync batch, you’ll need to temporarily re-enable password auth on the P16g for additional transfers:
Run the rsync, then lock down again:
|
Lock Down sshd (after rsync complete)
Revert the temporary password auth — back to pubkey-only. Use the verify-before/apply/verify-after pattern.
PasswordAuthentication:
# BEFORE: find the actual line (may be commented with #)
grep 'PasswordAuthentication' /etc/ssh/sshd_config | grep -v PAM | grep -v Depending
# APPLY: handle both commented and uncommented states
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
# VERIFY
grep '^PasswordAuthentication' /etc/ssh/sshd_config
KbdInteractiveAuthentication:
# BEFORE
grep 'KbdInteractiveAuthentication' /etc/ssh/sshd_config.d/99-archlinux.conf
# APPLY
sudo sed -i 's/^KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config.d/99-archlinux.conf
# VERIFY
grep 'KbdInteractiveAuthentication' /etc/ssh/sshd_config.d/99-archlinux.conf
Restart and confirm runtime config:
sudo systemctl restart sshd
sudo sshd -T | grep -i 'passwordauthentication\|kbdinteractive'
Expected (locked down)
passwordauthentication no
kbdinteractiveauthentication no
^\? in sed matches lines starting with optional — handles both commented and uncommented states in one command.
|
Verify rsync (from P16g)
ls ~/.ssh/id_ed25519_github*
ls ~/.gnupg/private-keys-v1.d/
ls ~/.age/identities/
ls ~/.local/share/gopass/stores/
ls ~/.secrets/
Step 2: Install Required Packages (P16g)
Check before installing
Always verify what’s already installed and what’s available before running pacman -S:
# Check if a package is already installed
pacman -Q <package-name>
# Search repos for a package (fuzzy match)
pacman -Ss <search-term>
# Find which package provides a missing library
pacman -F <library-name>.so
# Check what package owns an installed file (on a working machine)
pacman -Qo /usr/lib/<library-name>.so
Install
sudo pacman -S gnupg gopass pcsc-tools ccid yubikey-manager gocryptfs age rsync stow pinentry kwindowsystem
| Package | Why |
|---|---|
|
GPG encryption — gopass backend, YubiKey smartcard |
|
Password store — SSH key passphrases, dsource credentials |
|
Smartcard reader drivers for YubiKey |
|
YubiKey management CLI ( |
|
Encrypted filesystem — Claude Code credentials, sensitive configs |
|
Modern file encryption — SSH config in dots-quantum is age-encrypted |
|
File sync — bootstrap secrets from Razer |
|
Symlink farm manager — dots-quantum package deployment |
|
GPG passphrase prompt — without it, |
|
Required by |
sudo systemctl enable --now pcscd
Step 3: Verify GPG + YubiKey
Clear stale GPG locks
rsync’d GPG databases may have stale lock files from the source machine. Clear them first:
find ~/.gnupg -name "*.lock" -delete 2>/dev/null
find ~/.gnupg -name ".#*" -delete 2>/dev/null
gpgconf --kill all
Verify keys loaded
gpg -K
Should show your key with sec and ssb entries. If gpg -K hangs with "waiting for lock", repeat the lock cleanup above.
Verify YubiKey (SSH FIDO2, not GPG)
The YubiKey is used for SSH FIDO2 keys (id_ed25519_sk_*), not for GPG encryption. GPG private keys are stored locally.
# Verify YubiKey is detected
ykman list
Expected output
YubiKey 5C NFC (5.7.1) [OTP+FIDO+CCID] Serial: 31311804
# Verify GPG decryption works with LOCAL keys (no YubiKey touch needed)
export GPG_TTY=$(tty)
echo "test" | gpg --encrypt --recipient A5DF5F8EC04A5EE15E91188228A3183647525597 | gpg --decrypt
Step 4: Verify gopass
Fix gopass root store path
The rsync’d gopass config may reference ~/.password-store (legacy path) but the actual root store is at ~/.local/share/gopass/stores/root. Fix it:
# BEFORE
grep 'path.*password-store' ~/.config/gopass/config
# FIX (only needed if it points to ~/.password-store)
sed -i 's|path = /home/evanusmodestus/.password-store|path = /home/evanusmodestus/.local/share/gopass/stores/root|' ~/.config/gopass/config
# VERIFY
cat ~/.config/gopass/config
Set GPG TTY
GPG needs to know the terminal for passphrase prompts. Without this, decryption fails with "Inappropriate ioctl for device":
export GPG_TTY=$(tty)
Test gopass
gopass ls | head -10
# Test decryption — GPG prompts for passphrase (local key, NOT YubiKey)
gopass show v3/domains/d000/identity/ssh/github
gopass decrypts with local GPG private keys, NOT the YubiKey. The YubiKey is used for SSH FIDO2 authentication (sk keys), not GPG/gopass in this setup.
|
Step 5: Load SSH Keys
# Start ssh-agent if not running
eval "$(ssh-agent -s)"
# Get GitHub key passphrase from gopass
# Use -f (not -c) over SSH — no clipboard available without wl-clipboard
gopass show -f v3/domains/d000/identity/ssh/github
# Copy the passphrase output, then add the key
ssh-add ~/.ssh/id_ed25519_github
# Paste passphrase when prompted
# Verify
ssh-add -l
Test Git Remote Access
The iPSK VLAN (DOMUS-IoT) blocks outbound port 22. GitHub supports SSH over port 443 via ssh.github.com. You MUST specify -l git (user=git) — without it, SSH sends your local username and GitHub rejects it.
# Test GitHub — port 443 (port 22 blocked on iPSK VLAN)
# -F /dev/null = bypass SSH config (not stowed yet)
# -l git = login as "git" (GitHub requires this, not your username)
# -p 443 = SSH over HTTPS port
ssh -F /dev/null -i ~/.ssh/id_ed25519_github -T -p 443 -l git ssh.github.com
Expected output
Hi EvanusModestus! You've successfully authenticated, but GitHub does not provide shell access.
Once you move to EAP-TLS (Phase 8b) on the DOMUS-Secure VLAN, port 22 will be open and the normal ssh -T git@github.com will work. The port 443 workaround is only needed on the iPSK VLAN.
|
# Test GitLab (also port 443 if blocked)
ssh -F /dev/null -i ~/.ssh/id_ed25519_gitlab -T -p 443 -l git altssh.gitlab.com 2>/dev/null \
|| ssh -F /dev/null -i ~/.ssh/id_ed25519_gitlab -T git@gitlab.com
# Test Gitea (internal — port 2222, should work on iPSK VLAN)
ssh -F /dev/null -i ~/.ssh/id_ed25519_gitea -p 2222 git@gitea-01.inside.domusdigitalis.dev
Step 6: Create Directory Structure
mkdir -p ~/atelier/{_bibliotheca,_projects/personal}
Step 7: Clone dots-quantum
# Use SSH over port 443 (port 22 blocked on iPSK VLAN)
GIT_SSH_COMMAND="ssh -F /dev/null -i ~/.ssh/id_ed25519_github -p 443 -l git" \
git clone ssh://ssh.github.com:443/EvanusModestus/dots-quantum.git \
~/atelier/_projects/personal/dots-quantum
The ssh:// URL format with :443 is required for non-standard ports. The usual git@github.com:user/repo.git syntax doesn’t support port specification.
|
Step 8: Install CLI Tools (stow dependencies)
sudo pacman -S fzf fd ripgrep bat eza jq yq lazygit tmux stow
yay -S oh-my-posh-bin tmuxinator
Step 9: Stow Tier 1 (Essential)
Remove default shell configs (conflict with stow)
Arch creates default .bashrc and .bash_profile during install. stow can’t overwrite real files with symlinks — remove them first:
rm -f ~/.bashrc ~/.bash_profile ~/.zshrc
cd ~/atelier/_projects/personal/dots-quantum
stow -t ~ \
zsh bash shell git \
hyprland waybar wofi mako \
kitty oh-my-posh \
bin share \
btop fastfetch fzf fd ripgrep \
claude
Verify Stow
ls -la ~/.zshrc
ls -la ~/.config/hypr/hyprland.conf
ls -la ~/.config/waybar/config.jsonc
Step 10: Decrypt SSH Config from dots-quantum
The SSH config is age-encrypted in dots-quantum. Now that age keys are available:
cd ~/atelier/_projects/personal/dots-quantum
age -d -i ~/.age/identities/personal.key ssh/.ssh/config.age >| ssh/.ssh/config
# Stow SSH package (overwrites the rsync'd config with the stow symlink)
stow -t ~ ssh
# Verify config is symlinked
ls -la ~/.ssh/config
Step 11: Neovim
Install Mason dependencies
Mason auto-installs LSP servers, formatters, linters, and DAP adapters. These need language runtimes present:
sudo pacman -S python curl wget lua luarocks go
# Python package management (Mason uses pip internally)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Rust toolchain (Mason installs rust_analyzer)
rustup default stable
| Runtime | What Mason installs with it |
|---|---|
|
pyright (LSP), ruff (formatter), debugpy (DAP), pylint |
|
ts_ls, eslint_d, prettier, jsonls, yamlls, bashls, marksman |
|
lua_ls (LSP), stylua (formatter), luacheck (linter) |
|
gopls (LSP), golangci-lint |
|
rust_analyzer (LSP), codelldb (DAP) |
|
Mason downloads binaries |
Clone domus-nvim
# If on DOMUS-Secure VLAN (EAP-TLS), port 22 works directly
git clone git@github.com:EvanusModestus/domus-nvim.git ~/atelier/_projects/personal/domus-nvim
# If still on iPSK VLAN (port 22 blocked), use port 443 workaround
# GIT_SSH_COMMAND="ssh -F /dev/null -i ~/.ssh/id_ed25519_github -p 443 -l git" \
# git clone ssh://ssh.github.com:443/EvanusModestus/domus-nvim.git \
# ~/atelier/_projects/personal/domus-nvim
Symlink domus-nvim
|
Do NOT symlink to The dots-quantum The symlink MUST match the
If you symlink to |
# Symlink to nvim-domus (matches NVIM_APPNAME in .zshrc)
ln -sf ~/atelier/_projects/personal/domus-nvim ~/.config/nvim-domus
# Verify — should show the NVIM_APPNAME and matching symlink
echo "NVIM_APPNAME: $NVIM_APPNAME"
ls -la ~/.config/nvim-domus
# First launch — lazy.nvim installs plugins, Mason installs LSP servers +
# formatters + linters + DAP adapters, Treesitter downloads parsers.
# Takes 2-3 minutes. Wait for it to finish before quitting.
nvim
The aliases v (domus-nvim), ncore, nmod, nfidus in .zshrc and aliases.sh switch between configs by changing NVIM_APPNAME per-invocation. nvim without an alias uses the global NVIM_APPNAME="nvim-domus" — which is domus-nvim.
|
Step 12: Stow Tier 2
cd ~/atelier/_projects/personal/dots-quantum
stow -t ~ \
tmux vim lazygit \
vscodium zathura thunar \
libvirt ghostty \
gpg aider opencode
Step 13: Source New Shell
# Reload zsh with stowed config
exec zsh