Author SHA1 Message Date
sascha.koenig fe3f6ae288 dolt remote info 2026-08-31 18:24:03 +02:00
123 changed files with 7 additions and 7265 deletions
-80
View File
@@ -1,80 +0,0 @@
---
name: beads
description: Use when working in a repository that uses bd or Beads for durable project task tracking, issue dependencies, blocker management, multi-session handoff, or shared work memory. Trigger when the user asks to find ready work, claim or close tasks, create follow-up work, inspect blockers, recover project context, or choose between local planning and persistent project tracking.
---
# Beads
Use Beads as the shared project task system. Local plans, scratch files, and personal memories are useful, but they are not the durable source of truth for project work.
## First Step
Run:
```bash
bd prime
```
If that prints nothing, check whether the repository has an active Beads workspace:
```bash
bd where
```
## Preferred Route
Use the `bd` CLI when shell access is available. It is the most compact and direct Beads interface.
## Core CLI Workflow
1. Find work:
```bash
bd ready
bd list --status=open
bd list --status=in_progress
```
2. Inspect before editing:
```bash
bd show <id>
```
3. Claim work atomically:
```bash
bd update <id> --claim
```
4. Create durable follow-up work when implementation reveals new tasks:
```bash
bd create "Short title" --description="Why this exists and what needs to be done" --type=task --priority=2
```
5. Close completed work:
```bash
bd close <id> --reason="Completed"
```
## What Belongs In Beads
Use Beads for:
- shared project tasks
- blockers and dependencies
- discovered follow-up work
- work that must survive thread reset, compaction, or handoff
- status that another person or agent should be able to resume
Use agent-local planning tools only for the current turn's execution checklist. Do not treat them as shared project state.
## Rules
- Do not create markdown TODO files as the source of truth when Beads is available.
- Do not use `bd edit`; it opens an interactive editor. Use `bd update` flags instead.
- Prefer `--json` when parsing `bd` output programmatically.
- If hooks are installed, `bd prime` may already be injected. Run it manually when context is missing.
- Do not auto-close or mutate tasks unless the work is actually complete.
-4
View File
@@ -1,4 +0,0 @@
interface:
display_name: "Beads"
short_description: "Project task tracking with bd"
default_prompt: "Use $beads to inspect ready work and manage durable project tasks."
-77
View File
@@ -1,77 +0,0 @@
# Dolt database (managed by Dolt, not git)
dolt/
embeddeddolt/
proxieddb/
# Runtime files
bd.sock
bd.sock.startlock
sync-state.json
last-touched
.exclusive-lock
# Daemon runtime (lock, log, pid)
daemon.*
# Push state (runtime, per-machine)
push-state.json
# Lock files (various runtime locks)
*.lock
# Credential key (encryption key for federation peer auth — never commit)
.beads-credential-key
# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
proxied_server_client_info.json
# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect
# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
export-state/
export-state.json
last_pull
# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
ephemeral.sqlite3
ephemeral.sqlite3-journal
ephemeral.sqlite3-wal
ephemeral.sqlite3-shm
# Dolt server management (auto-started by bd)
dolt-server.pid
dolt-server.log
dolt-server.lock
dolt-server.port
dolt-server.activity
# Debug-mode pprof artifacts (written when dolt.debug: true in config.yaml)
dolt-pprof/
# Corrupt backup directories (created by bd doctor --fix recovery)
*.corrupt.backup/
# Backup data (auto-exported JSONL, local-only)
backup/
# Per-project environment file (Dolt connection config, GH#2520)
.env
# Legacy files (from pre-Dolt versions)
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
db.sqlite
bd.db
# NOTE: Do NOT add negation patterns here.
# They would override fork protection in .git/info/exclude.
# Config files (metadata.json, config.yaml) are tracked by git by default
# since no pattern above ignores them.
-81
View File
@@ -1,81 +0,0 @@
# Beads - AI-Native Issue Tracking
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
## What is Beads?
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
## Quick Start
### Essential Commands
```bash
# Create new issues
bd create "Add user authentication"
# View all issues
bd list
# View issue details
bd show <issue-id>
# Update issue status
bd update <issue-id> --claim
bd update <issue-id> --status done
# Sync with Dolt remote
bd dolt push
```
### Working with Issues
Issues in Beads are:
- **Git-native**: Stored in Dolt database with version control and branching
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
- **Branch-aware**: Issues can follow your branch workflow
- **Sync-ready**: Uses Dolt remotes for backup and team sharing
## Why Beads?
✨ **AI-Native Design**
- Built specifically for AI-assisted development workflows
- CLI-first interface works seamlessly with AI coding agents
- No context switching to web UIs
🚀 **Developer Focused**
- Issues live in your repo, right next to your code
- Works offline, syncs when you push
- Fast, lightweight, and stays out of your way
🔧 **Git Integration**
- Dolt-native sync via bd dolt push / bd dolt pull
- Branch-aware issue tracking
- Dolt-native three-way merge resolution
## Get Started with Beads
Try Beads in your own projects:
```bash
# Install Beads
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
# Initialize in your repo
bd init
# Create your first issue
bd create "Try out Beads"
```
## Learn More
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
- **Quick Start Guide**: Run `bd quickstart`
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
---
*Beads: Issue tracking that moves at the speed of thought* ⚡
-70
View File
@@ -1,70 +0,0 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
# issue-prefix: ""
# Use no-db mode: JSONL-only, no Dolt database
# When true, .beads/issues.jsonl is the only local store
# no-db: false
# Enable JSON output by default
# json: false
# Feedback title formatting for mutating commands (create/update/close/dep/edit)
# 0 = hide titles, N > 0 = truncate to N characters
# output:
# title-length: 255
# Default actor for audit trails (overridden by BEADS_ACTOR or --actor)
# actor: ""
# Export events (audit trail) to .beads/events.jsonl on each flush/sync
# When enabled, new events are appended incrementally using a high-water mark.
# Use 'bd export --events' to trigger manually regardless of this setting.
# events-export: false
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct database
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# Dolt-native backup (periodic backup for off-machine recovery)
# This is full database backup only. Cross-machine sync uses Dolt remotes.
# backup:
# enabled: false # Disable auto-backup entirely
# interval: 15m # Minimum time between auto-backups
# git-push: false # Disable git push (backup locally only)
# git-repo: "" # Separate git repo for backups (default: project repo)
# Optional JSONL auto-export for viewers, interchange, and issue-level migration.
# Disabled by default; enable only when an integration needs fresh .beads/issues.jsonl.
# Use relative paths under .beads/ for JSONL import/export filenames.
# export:
# auto: false
# path: issues.jsonl
# interval: 60s
# git-add: false
# import:
# path: issues.jsonl
# Integration settings (access with 'bd config get/set')
# Non-secret keys (stored in the database):
# - jira.url, jira.project
# - linear.team_id
# - github.org, github.repo
#
# Secret keys (stored in this file but prefer env vars to avoid git exposure):
# - linear.api_key → use LINEAR_API_KEY env var instead
# - github.token → use GITHUB_TOKEN env var instead
sync.remote: "git+ssh://gitea@git.az-gruppe.com/AZ-Intec-GmbH/AZ-NIX-CLIENTS.git"
export:
auto: true
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run post-checkout "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run post-checkout "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run post-checkout "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'post-checkout' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run post-checkout "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'post-checkout' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'post-checkout'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run post-merge "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run post-merge "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run post-merge "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'post-merge' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run post-merge "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'post-merge' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'post-merge'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run pre-commit "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run pre-commit "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run pre-commit "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'pre-commit' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run pre-commit "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'pre-commit' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'pre-commit'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run pre-push "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run pre-push "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run pre-push "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'pre-push' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run pre-push "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'pre-push' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'pre-push'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v1.2.2 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
_bd_timeout=${BEADS_HOOK_TIMEOUT:-300}
_bd_used_perl=0
if command -v timeout >/dev/null 2>&1; then
timeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
elif command -v perl >/dev/null 2>&1; then
_bd_used_perl=1
perl -e 'alarm shift; exec @ARGV' "$_bd_timeout" bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
else
echo >&2 "beads: hook 'prepare-commit-msg' running without timeout; install coreutils or perl to enable BEADS_HOOK_TIMEOUT"
bd hooks run prepare-commit-msg "$@"
_bd_exit=$?
fi
if [ $_bd_exit -eq 124 ] || { [ $_bd_used_perl -eq 1 ] && [ $_bd_exit -eq 142 ]; }; then
echo >&2 "beads: hook 'prepare-commit-msg' timed out after ${_bd_timeout}s — continuing without beads"
_bd_exit=0
fi
if [ $_bd_exit -eq 3 ]; then
echo >&2 "beads: database not initialized — skipping hook 'prepare-commit-msg'"
_bd_exit=0
fi
if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION v1.2.2 ---
View File
-7
View File
@@ -1,7 +0,0 @@
{
"database": "dolt",
"backend": "dolt",
"dolt_mode": "embedded",
"dolt_database": "AZ_NIX_CLIENTS",
"project_id": "eaeef29c-e367-4856-b22a-27a3b54a949a"
}
-15
View File
@@ -1,15 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"command": "bd prime --hook-json",
"type": "command"
}
],
"matcher": ""
}
]
}
}
-2
View File
@@ -1,2 +0,0 @@
[features]
hooks = true
-51
View File
@@ -1,51 +0,0 @@
{
"hooks": {
"PostCompact": [
{
"hooks": [
{
"command": "bd codex-hook PostCompact",
"statusMessage": "Scheduling Beads context refresh",
"type": "command"
}
],
"matcher": "manual|auto"
}
],
"PreCompact": [
{
"hooks": [
{
"command": "bd codex-hook PreCompact",
"statusMessage": "Checking Beads context",
"type": "command"
}
],
"matcher": "manual|auto"
}
],
"SessionStart": [
{
"hooks": [
{
"command": "bd codex-hook SessionStart",
"statusMessage": "Loading Beads context",
"type": "command"
}
],
"matcher": "startup|resume|clear"
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"command": "bd codex-hook UserPromptSubmit",
"statusMessage": "Refreshing Beads context",
"type": "command"
}
]
}
]
}
}
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env bash
# Activate the devshell from the Nix flake
# This loads all tools and environment variables defined in flake.nix
use flake
-3
View File
@@ -1,3 +0,0 @@
# Use bd merge for beads JSONL files
.beads/issues.jsonl merge=beads
-46
View File
@@ -1,46 +0,0 @@
# Sisyphus work session data
.sisyphus/
# Editor files
*~
.*.swp
.*.swo
.*.swx
# Build artifacts
result
result-*
.direnv/
# IDE
.vscode/
.idea/
*.iml
# OS
.DS_Store
Thumbs.db
# Opencode / AI Agent rules (generated by devShell)
.opencode-rules/
coding-rules.json
# Pi / Pi Lens local agent artifacts
.pi/
.pi-lens/
.pi-lens*
pi.lens*
.todos/
.sidecar/
# Per-host age identities injected by the deployment wrapper
.secrets/identities/
# Local VM images
*.qcow2
# Beads / Dolt files (added by bd init)
.dolt/
*.db
.beads-credential-key
.beads/proxieddb/
-1
View File
@@ -1 +0,0 @@
/nix/store/7jh7qhfs4gwcrzbpc3p0w9scdqr8vrzb-source/rules
-125
View File
@@ -1,125 +0,0 @@
# Agent Instructions
## MANDATORY: Use td for Task Management
You must run td usage --new-session at conversation start (or after /clear) to see current work.
Use td usage -q for subsequent reads.
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
## Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
bd sync # Sync with git
```
## Landing the Plane (Session Completion)
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd sync
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:970c3bf2 -->
## Beads Issue Tracker
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```
### Rules
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
## Agent Context Profiles
The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.
- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.
## Session Completion
This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.
1. **File issues for remaining work** - Create beads for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **Handle git/sync by active profile**:
```bash
# Conservative/minimal/default: report status and proposed commands; wait for approval.
git status
# Team-maintainer opt-in only, unless current instructions forbid it:
git pull --rebase
bd dolt push
git push
git status
```
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step
**Critical rules:**
- Explicit user or orchestrator instructions override this Beads block.
- Do not commit or push without clear authority from the active profile or the current user request.
- If a required sync or push is blocked, stop and report the exact command and error.
<!-- END BEADS INTEGRATION -->
<!-- BEGIN BEADS CODEX SETUP: generated by bd setup codex -->
## Beads Issue Tracker
Use Beads (`bd`) for durable task tracking in repositories that include it. Use the `beads` skill at `.agents/skills/beads/SKILL.md` (project install) or `~/.agents/skills/beads/SKILL.md` (global install) for Beads workflow guidance, then use the `bd` CLI for issue operations.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
bd prime # Refresh Beads context
```
### Rules
- Use `bd` for all task tracking; do not create markdown TODO lists.
- Run `bd prime` when Beads context is missing or stale. Codex 0.129.0+ can load Beads context automatically through native hooks; use `/hooks` to inspect or toggle them.
- Keep persistent project memory in Beads via `bd remember`; do not create ad hoc memory files.
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
<!-- END BEADS CODEX SETUP -->
-77
View File
@@ -1,77 +0,0 @@
# Project Instructions for AI Agents
This file provides instructions and context for AI coding agents working on this project.
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:6cd5cc61 -->
## Beads Issue Tracker
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
### Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```
### Rules
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
## Agent Context Profiles
The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.
- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.
## Session Completion
This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.
1. **File issues for remaining work** - Create beads for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **Handle git/sync by active profile**:
```bash
# Conservative/minimal/default: report status and proposed commands; wait for approval.
git status
# Team-maintainer opt-in only, unless current instructions forbid it:
git pull --rebase
git push
git status
```
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step
**Critical rules:**
- Explicit user or orchestrator instructions override this Beads block.
- Do not commit or push without clear authority from the active profile or the current user request.
- If a required sync or push is blocked, stop and report the exact command and error.
<!-- END BEADS INTEGRATION -->
## Build & Test
_Add your build and test commands here_
```bash
# Example:
# npm install
# npm test
```
## Architecture Overview
_Add a brief overview of your project architecture_
## Conventions & Patterns
_Add your project-specific conventions here_
-47
View File
@@ -1,47 +0,0 @@
# AZ-NIX-CLIENTS
A NixOS flake that manages AzIntec client hosts. Two host species live in this
repo: developer workstations (e.g. `AZ-LT-NIX`) and the thin-client fleet
(`AZ-TC-NN`).
## Language
**Thin Client**:
A NixOS client host in the `AZ-TC-NN` fleet — a mini-PC running KDE Plasma that
authenticates against Active Directory, provides a pre-configured RDP shortcut
to a terminal server, and runs a pinned browser for one web app. Replaces a
Windows 11 workstation.
_Avoid_: kiosk, terminal, workstation
**Workstation**:
A NixOS host used for interactive development work (e.g. `AZ-LT-NIX`). Not a
Thin Client — these hosts use the m3ta-home profile system and are not in the
production fleet.
_Avoid_: desktop, laptop, client
**Terminal Server**:
The remote Windows RDS host (or farm) that Thin Clients connect to via RDP.
Kerberos SSO is expected to flow from the client login to this server.
_Avoid_: RDP server, remote desktop, RD server
**Web App**:
The single browser-based application that Thin Clients open at session start.
Kerberos SSO is expected for this app.
_Avoid_: portal, intranet, app
**Domain User**:
An identity provided by Active Directory via `sssd`. Logs into the Thin Client
at the SDDM login screen; credentials flow to RDP and the web app via Kerberos.
_Avoid_: AD user, network user, SSO user
**Hardware Class**:
One of the 2–3 mini-PC SKUs the fleet is composed of (e.g. Dell OptiPlex Micro,
Lenovo ThinkCentre Tiny). Each class has its own hardware module under the
thin-client role.
_Avoid_: SKU, model, hardware type
**Fleet Host**:
A single physical Thin Client, identified by `AZ-TC-NN` (two-digit number).
Each fleet host has a tiny `default.nix` that imports the thin-client role and
declares its hardware class and hostname.
_Avoid_: node, device, machine
+7
View File
@@ -0,0 +1,7 @@
This repository is being used as a Dolt remote.
ref=refs/dolt/data
head=9e0218094766fa36c5298c84228fb41f50b37d1e
timestamp=2026-08-31T16:24:03Z
@@ -1,634 +0,0 @@
# 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` — encrypted `Pluto` password; 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:
```bash
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:
```nix
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:
```bash
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-keytab` but no WiFi certificate or PSK yet.
- [ ] **Step 4: Commit the host AD configuration**
```bash
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:
```bash
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:
```nix
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:
```nix
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:
```nix
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:
```bash
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**
```bash
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`:
```nix
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:
```nix
# - <host>-wifi-psk.age — temporary WPA-Personal password
```
Add this AZ-TC-01 recipient rule next to its other per-host secrets:
```nix
"secrets/AZ-TC-01-wifi-psk.age".publicKeys = [AZ-TC-01] ++ users;
```
- [ ] **Step 3: Verify mode-specific secret selection**
Run:
```bash
nix eval --json path:.#nixosConfigurations.AZ-TC-01.config.age.secrets \
| jq -r 'keys[]' \
| sort
```
Expected output contains exactly these feature-specific entries:
```text
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:
```bash
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**
```bash
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:
```bash
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**
```bash
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**
```bash
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:
```bash
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**
```bash
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`:
```bash
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**
```bash
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**
```bash
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**
```bash
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:
```markdown
- 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:
```bash
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:
```bash
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**
```bash
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**
```bash
nix fmt -- --check .
nix flake check
```
Expected: both commands exit `0`.
- [ ] **Step 2: Confirm only the intended secrets are required**
```bash
nix eval --json path:.#nixosConfigurations.AZ-TC-01.config.age.secrets \
| jq -r 'keys[]' \
| sort
```
Expected feature-specific keys:
```text
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:
```bash
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**
```bash
nix build --no-link .#nixosConfigurations.AZ-TC-01.config.system.build.toplevel
```
Expected: exit `0`.
- [ ] **Step 5: Review the implementation commits**
```bash
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:
```bash
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**
```bash
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**
```bash
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**
```bash
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.
@@ -1,76 +0,0 @@
# AZ-TC-01 AD and WiFi bootstrap design
## Goal
Bring `AZ-TC-01` online as a reproducible pilot in two independent stages:
1. Integrate the host with Active Directory.
2. Connect it temporarily to the WPA-Personal network `Pluto` using a shared password.
Certificate-based access to the `Saturn` network remains out of scope until AD is proven functional.
## Host profile
`AZ-TC-01` remains on the `staging` site so unrelated production integrations stay disabled. Only AD and WiFi are enabled explicitly.
The host uses these confirmed infrastructure values:
- AD DNS domain: `az-group.local`
- Kerberos realm: `AZ-GROUP.LOCAL`
- Primary domain controller: `azdc01.az-group.local` (`192.168.152.253`)
- Secondary domain controller: `adpdc01.az-group.local` (`192.168.152.254`)
- Computer location: default `CN=Computers,DC=az-group,DC=local` container
- Temporary WPA-Personal SSID: `Pluto`
## Active Directory bootstrap
An administrator creates or joins the `AZ-TC-01` computer account from an admin workstation that can reach the domain controllers. `adcli` writes a binary host keytab for `AZ-TC-01.az-group.local` without altering the admin workstation's own keytab.
The binary keytab is encrypted directly from the file into `secrets/AZ-TC-01-krb5-keytab.age`; it must never be copied through a text editor. The current encrypted placeholder is replaced. Before deployment, the decrypted result is checked with `klist -k -e` and must contain machine and host principals in `AZ-GROUP.LOCAL`.
At boot, agenix decrypts it to `/etc/krb5.keytab` with mode `0600`. SSSD starts only after the age identity is available. Runtime verification covers Kerberos keytab readability, SSSD domain status, identity lookup, and an interactive AD login.
## Temporary WPA-Personal WiFi
The WiFi module supports two explicit modes:
- `psk`: WPA-Personal using a per-host agenix secret.
- `eap-tls`: the existing certificate-based configuration for the later `Saturn` rollout.
For this pilot, `AZ-TC-01` selects `psk` and SSID `Pluto`. The shared password is entered locally into `secrets/AZ-TC-01-wifi-psk.age`; it is never sent in chat or stored in Nix source.
Agenix decrypts the password to a root-only runtime file. NetworkManager obtains the PSK at activation time without placing its plaintext in the Nix store. Enabling `psk` requires only the PSK secret; enabling `eap-tls` requires only the client certificate secret. This keeps the deferred `Saturn` certificate from blocking the pilot.
## Secret and deployment flow
The existing dedicated host age identity remains the recipient for both per-host secrets:
- `AZ-TC-01-krb5-keytab.age`
- `AZ-TC-01-wifi-psk.age`
The deployment preflight evaluates the enabled features, confirms each required encrypted file exists, and verifies that the injected host identity can decrypt it. The identity is copied to `/var/lib/agenix/identity.age` during installation.
## Validation
Before touching the target disk:
1. Evaluate the host configuration and inspect its required secrets.
2. Validate the decrypted keytab with `klist -k -e`.
3. Run the deployment wrapper's non-destructive preflight.
4. Run the Nix flake checks and build the host closure.
After boot:
1. Confirm NetworkManager is connected to `Pluto` and DNS resolves both domain controllers.
2. Confirm Kerberos ports and time synchronization are available.
3. Check `systemctl status sssd` and `sssctl domain-status az-group.local`.
4. Resolve an AD user with `getent passwd` and `id`.
5. Perform an SDDM login with an AD account and verify offline credential caching only after one successful online login.
## Failure handling and rollback
- A missing or undecryptable required secret blocks deployment.
- An invalid keytab prevents SSSD startup and is diagnosed before deployment with `klist`.
- AD and WiFi stay independently switchable, so either can be disabled while diagnosing the other.
- Returning the host to base staging requires setting both feature flags to `false`; no production integrations are enabled implicitly.
- The future migration to `Saturn` changes the WiFi mode to `eap-tls`, replaces the SSID, installs the trusted RADIUS CA, and adds the client certificate secret. It does not alter the AD design.
Generated
-1445
View File
File diff suppressed because it is too large Load Diff
-144
View File
@@ -1,144 +0,0 @@
{
description = ''
For questions just DM me on X: https://twitter.com/@m3tam3re
There is also some NIXOS content on my YT channel: https://www.youtube.com/@m3tam3re
One of the best ways to learn NIXOS is to read other peoples configurations. I have personally learned a lot from Gabriel Fontes configs:
https://github.com/Misterio77/nix-starter-configs
https://github.com/Misterio77/nix-config
Please also check out the starter configs mentioned above.
'';
inputs = {
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-25.11";
m3ta-nixpkgs.url = "git+https://code.m3ta.dev/m3tam3re/nixpkgs";
m3ta-home = {
# url = "path:/home/sascha.koenig/p/NIX/m3ta-home";
url = "git+ssh://gitea@code.m3ta.dev/m3tam3re/m3ta-home";
inputs.nixpkgs.follows = "nixpkgs";
};
llm-agents.url = "github:numtide/llm-agents.nix";
nur = {
url = "github:nix-community/NUR";
inputs.nixpkgs.follows = "nixpkgs";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
agenix.url = "github:ryantm/agenix";
nixos-anywhere = {
url = "github:nix-community/nixos-anywhere";
inputs.nixpkgs.follows = "nixpkgs";
};
agents = {
# url = "path:/home/m3tam3re/p/AI/AGENTS";
url = "git+ssh://gitea@code.m3ta.dev/m3tam3re/AGENTS";
};
nix-colors.url = "github:misterio77/nix-colors";
};
outputs = {
self,
agenix,
disko,
nixpkgs,
m3ta-nixpkgs,
...
} @ inputs: let
inherit (self) outputs;
systems = [
"aarch64-linux"
"i686-linux"
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs systems;
in {
packages =
forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system});
overlays = let
all = import ./overlays {inherit inputs;};
in
removeAttrs all ["mkLlmAgentsOverlay"];
lib.mkLlmAgentsOverlay = (import ./overlays {inherit inputs;}).mkLlmAgentsOverlay;
devShells = forAllSystems (system: let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true; # Allow unfree packages in devShell
};
in {
default = pkgs.mkShell {
buildInputs = with pkgs; [
alejandra
nixd
openssh
agenix.packages.${system}.default
statix
deadnix
];
};
});
nixosConfigurations = {
AZ-LT-NIX = inputs.nixpkgs.lib.nixosSystem {
specialArgs = {
inherit inputs outputs;
system = "x86_64-linux";
};
modules = [
./hosts/AZ-LT-NIX
agenix.nixosModules.default
inputs.home-manager.nixosModules.home-manager
m3ta-nixpkgs.nixosModules.default
];
};
# ── Thin Client fleet (AZ-TC-NN) ──────────────────────────────
# Each host is a minimal wrapper around the thin-client role.
# Add new hosts by:
# 1. Create hosts/AZ-TC-NN/default.nix (copy from AZ-TC-01).
# 2. Add an entry here.
# 3. Run `agenix -e secrets/AZ-TC-NN-*.age` to provision secrets.
AZ-TC-01 = inputs.nixpkgs.lib.nixosSystem {
specialArgs = {inherit inputs outputs; system = "x86_64-linux";};
modules = [
disko.nixosModules.disko
agenix.nixosModules.default
./hosts/AZ-TC-01
];
};
AZ-TC-02 = inputs.nixpkgs.lib.nixosSystem {
specialArgs = {inherit inputs outputs; system = "x86_64-linux";};
modules = [
disko.nixosModules.disko
agenix.nixosModules.default
./hosts/AZ-TC-02
];
};
AZ-TC-03 = inputs.nixpkgs.lib.nixosSystem {
specialArgs = {inherit inputs outputs; system = "x86_64-linux";};
modules = [
disko.nixosModules.disko
agenix.nixosModules.default
./hosts/AZ-TC-03
];
};
};
};
}
-165
View File
@@ -1,165 +0,0 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{
config,
pkgs,
...
}: {
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
# Bootloader.
boot.loader.systemd-boot.enable = true;
boot.initrd.kernelModules = ["amdgpu" "hid_asus"];
boot.kernelPackages = pkgs.linuxPackages_latest;
boot.kernelParams = ["pcie_aspm=off" "pcie_port_pm=off"];
boot.extraModprobeConfig = ''
options hid_asus enable_touchpad=1
options mt7925e disable_aspm=1
options mt7925_common disable_clc=1
'';
services.xserver.videoDrivers = ["amdgpu"];
security.polkit.enable = true;
security.pam.services.gdm.enableGnomeKeyring = true;
networking = {
wireless.iwd = {
enable = true;
settings = {
Settings = {
Timers = "DefaultRoamThreshold=30";
};
General = {
AddressRandomization = "network";
};
};
};
networkmanager = {
enable = true;
wifi = {
backend = "iwd";
powersave = false;
};
};
hostName = "AZ-LT-NIX";
};
systemd.services.disable-wifi-powersave = {
description = "Disable WiFi power save";
after = ["network-online.target" "iwd.service"];
wants = ["network-online.target"];
wantedBy = ["multi-user.target"];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = ''
${pkgs.bash}/bin/bash -c 'for i in {1..30}; do \
${pkgs.iw}/bin/iw dev wlan0 set power_save off 2>/dev/null && exit 0; \
sleep 1; \
done; exit 1'
'';
Restart = "on-failure";
RestartSec = "10s";
};
};
# Define your hostname.
# warp-terminal update fix
# networking.extraHosts = ''
# 127.0.0.1 releases.warp.dev
# 127.0.0.1 app.warp.dev
# '';
# Pick only one of the below networking options.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Set your time zone.
time.timeZone = "Europe/Berlin";
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Select internationalisation properties.
i18n.defaultLocale = "de_DE.UTF-8";
# console = {
# font = "Lat2-Terminus16";
# keyMap = "us";
# useXkbConfig = true; # use xkb.options in tty.
# };
# Enable the X11 windowing system.
# services.xserver.enable = true;
# Enable the GNOME Desktop Environment.
# services.xserver.displayManager.gdm.enable = true;
# services.xserver.desktopManager.gnome.enable = true;
# Configure keymap in X11
# services.xserver.xkb.layout = "us";
# services.xserver.xkb.options = "eurosign:e,caps:escape";
# Enable CUPS to print documents.
# services.printing.enable = true;
# Enable sound.
# hardware.pulseaudio.enable = true;
# OR
# Enable touchpad support (enabled default in most desktopManager).
# services.libinput.enable = true;
# Define a user account. Don't forget to set a password with ‘passwd’.
# List packages installed in system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [asusctl git];
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
services.openssh = {
enable = true;
settings.PermitRootLogin = "no";
settings = {
PasswordAuthentication = true;
};
};
services.fstrim.enable = true;
# Open ports in the firewall.
networking.firewall.allowedTCPPorts = [8080];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "25.05"; # Did you read the comment?
}
-64
View File
@@ -1,64 +0,0 @@
# A staring point is the basic NIXOS configuration generated by the ISO installer.
# On an existing NIXOS install you can use the following command in your flakes basedir:
# sudo nixos-generate-config --dir ./hosts/m3tam3re
#
# Please make sure to change the first couple of lines in your configuration.nix:
# { config, inputs, ouputs, lib, pkgs, ... }:
#
# {
# imports = [ # Include the results of the hardware scan.
# ./hardware-configuration.nix
# inputs.home-manager.nixosModules.home-manager
# ];
# ...
#
# Moreover please update the packages option in your user configuration and add the home-manager options:
# users.users = {
# m3tam3re = {
# isNormalUser = true;
# initialPassword = "12345";
# extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
# packages = [ inputs.home-manager.packages.${pkgs.system}.default ];
# };
# };
#
# home-manager = {
# useUserPackages = true;
# extraSpecialArgs = { inherit inputs outputs; };
# users.m3tam3re =
# import ../../home/m3tam3re/${config.networking.hostName}.nix;
# };
#
# Please also change your hostname accordingly:
#:w
# networking.hostName = "nixos"; # Define your hostname.
{...}: {
imports = [
../common
./configuration.nix
./hardware.nix
./programs.nix
./secrets.nix
./services
];
extraServices = {
flatpak.enable = true;
ollama.enable = true;
podman.enable = true;
virtualisation.enable = true;
};
services.ollama = {
environmentVariables = {
# HCC_AMDGPU_TARGET = "gfx1103";
# ROCR_VISIBLE_DEVICES = "0";
};
# rocmOverrideGfx = "11.0.3";
};
# System prerequisites formerly enabled transitively by the NixOS
# programs.dms-shell module. DMS itself is configured via m3ta-home.
services.power-profiles-daemon.enable = true;
services.accounts-daemon.enable = true;
hardware.i2c.enable = true;
hardware.graphics.enable = true;
}
@@ -1,68 +0,0 @@
# Do not modify this file! It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{
config,
lib,
modulesPath,
...
}: {
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = ["nvme" "xhci_pci" "thunderbolt" "usbhid" "usb_storage" "sd_mod" "sdhci_pci"];
boot.initrd.kernelModules = [];
boot.kernelModules = ["kvm-amd"];
boot.extraModulePackages = [];
fileSystems."/" = {
device = "/dev/disk/by-uuid/9fcbe547-12dc-467d-a0e2-cefeedaf28d9";
fsType = "btrfs";
options = ["subvol=root" "compress=zstd" "noatime" "ssd" "discard=async"];
};
fileSystems."/home" = {
device = "/dev/disk/by-uuid/9fcbe547-12dc-467d-a0e2-cefeedaf28d9";
fsType = "btrfs";
options = ["subvol=home" "compress=zstd" "noatime" "ssd" "discard=async"];
};
fileSystems."/nix" = {
device = "/dev/disk/by-uuid/9fcbe547-12dc-467d-a0e2-cefeedaf28d9";
fsType = "btrfs";
options = ["subvol=nix" "compress=zstd" "noatime" "ssd" "discard=async"];
};
fileSystems."/persist" = {
device = "/dev/disk/by-uuid/9fcbe547-12dc-467d-a0e2-cefeedaf28d9";
fsType = "btrfs";
options = ["subvol=persist" "compress=zstd" "noatime" "ssd" "discard=async"];
};
fileSystems."/var/log" = {
device = "/dev/disk/by-uuid/9fcbe547-12dc-467d-a0e2-cefeedaf28d9";
fsType = "btrfs";
options = ["subvol=log" "compress=zstd" "noatime" "ssd" "discard=async"];
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/89EE-C4CE";
fsType = "vfat";
options = ["fmask=0022" "dmask=0022"];
};
swapDevices = [
{device = "/dev/disk/by-uuid/7e78ee33-a051-439a-80aa-635d0ab698e4";}
];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp194s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
-16
View File
@@ -1,16 +0,0 @@
{
hardware = {
amdgpu.opencl.enable = true;
bluetooth.enable = true;
keyboard.zsa.enable = true;
graphics.enable = true;
};
# udev rules for vibetyper / uinput access (virtual input device injection)
services.udev.extraRules = ''
KERNEL=="uinput", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput"
KERNEL=="event*", SUBSYSTEM=="input", MODE="0660", GROUP="input"
'';
boot.kernelModules = ["uinput"];
}
-143
View File
@@ -1,143 +0,0 @@
# hosts/m3-kratos/home.nix — Host-specific home-manager overrides.
# AMD desktop: dual 2560x1440@144 via DisplayPort.
# Everything else (shell, editors, gaming, media, theme, etc.) comes from
# m3ta-home via the profile mapping in hosts/common/users/m3tam3re.nix.
{
config,
lib,
...
}:
with lib; let
mkEnvVar = name: value: {
_args = [
name
value
];
};
in {
imports = [
];
config = mkMerge [
# ── XDG / MIME defaults ──
{
xdg = {
enable = true;
userDirs.setSessionVariables = true;
configFile."mimeapps.list".force = true;
mimeApps = {
enable = true;
associations.added = {
"application/zip" = ["org.gnome.FileRoller.desktop"];
"application/csv" = ["calc.desktop"];
"application/pdf" = ["vivaldi-stable.desktop"];
"x-scheme-handler/http" = ["vivaldi-stable.desktop"];
"x-scheme-handler/https" = ["vivaldi-stable.desktop"];
};
defaultApplications = {
"application/zip" = ["org.gnome.FileRoller.desktop"];
"application/csv" = ["calc.desktop"];
"application/pdf" = ["vivaldi-stable.desktop"];
"application/md" = ["dev.zed.Zed.desktop"];
"application/text" = ["dev.zed.Zed.desktop"];
"x-scheme-handler/http" = ["vivaldi-stable.desktop"];
"x-scheme-handler/https" = ["vivaldi-stable.desktop"];
};
};
};
}
# ── Hyprland monitor layout ──
(mkIf config.desktop.wm.hyprland.enable {
wayland.windowManager.hyprland = {
enable = true;
settings = {
workspace_rule = [
{
workspace = "1";
monitor = "eDP-1";
default = true;
}
{
workspace = "2";
monitor = "eDP-1";
}
{
workspace = "3";
monitor = "DP-8";
}
{
workspace = "4";
monitor = "DP-8";
}
{
workspace = "5";
monitor = "DP-10";
}
{
workspace = "6";
monitor = "DP-10";
}
{
workspace = "7";
monitor = "DP-10";
}
];
# m3ta-home sets QT_QPA_PLATFORMTHEME to gtk3 globally for Hyprland.
# ksnip crashes with duplicate GDK type registration under that Qt GTK
# platform theme, so use qtct for Qt apps on this host instead.
"env" = mkForce [
(mkEnvVar "XCURSOR_SIZE" "32")
(mkEnvVar "HYPRCURSOR_THEME" "Bibata-Modern-Ice")
(mkEnvVar "WLR_NO_HARDWARE_CURSORS" "1")
(mkEnvVar "XDG_CURRENT_DESKTOP" "Hyprland")
(mkEnvVar "XDG_SESSION_TYPE" "wayland")
(mkEnvVar "XDG_SESSION_DESKTOP" "Hyprland")
(mkEnvVar "XKB_DEFAULT_LAYOUT" "de")
(mkEnvVar "NIXOS_OZONE_WL" "1")
(mkEnvVar "QT_QPA_PLATFORM" "wayland;xcb")
(mkEnvVar "QT_QPA_PLATFORMTHEME" "qt5ct")
(mkEnvVar "QT_QPA_PLATFORMTHEME_QT6" "qt6ct")
];
window_rule = [
{
match.class = "dev.zed.Zed";
workspace = "3";
}
{
match.class = "^(com.obsproject.Studio)$";
workspace = "1";
}
{
match.class = "^(brave-browse)$";
workspace = "4";
opacity = "1.0";
}
{
match.class = "^(vivaldi-stable)$";
workspace = "4";
opacity = "1.0";
}
{
match.initial_title = "3.basecamp.com_/5996442/";
workspace = "5";
opacity = "1.0";
tile = true;
}
{
match.initial_title = "teams.microsoft.com_/";
workspace = "6";
opacity = "1.0";
tile = true;
}
{
match.initial_title = "outlook.office.com_/mail/";
workspace = "6";
opacity = "1.0";
tile = true;
}
];
};
};
})
];
}
-51
View File
@@ -1,51 +0,0 @@
{
lib,
pkgs,
...
}: {
programs.nix-ld.enable = true;
programs.nix-ld.libraries = with pkgs; [
# Add any missing dynamic libraries for unpackaged programs
# here, NOT in environment.systemPackages
];
programs.hyprland = {
enable = true;
xwayland.enable = true;
withUWSM = true;
};
programs.fish.enable = true;
programs.localsend.enable = true;
programs.thunar = {
enable = true;
plugins = with pkgs; [thunar-archive-plugin thunar-volman];
};
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
pinentryPackage = pkgs.pinentry-gnome3;
settings = {default-cache-ttl = 10800;};
};
programs.obs-studio = {
enable = true;
enableVirtualCamera = true;
plugins = with pkgs.obs-studio-plugins; [
obs-composite-blur
obs-vaapi
# obs-vertical-canvas
obs-vkcapture
wlrobs
];
};
programs.nh = {
enable = true;
clean.enable = true;
clean.extraArgs = "--keep-since 4d --keep 3";
flake = "/home/m3tam3re/p/nixos/nixos-config";
};
# nh.clean and nix.gc.automatic conflict (NixOS assertion) — this host
# uses the nh clean timer above for generation/GC retention, so disable
# the plain nix-gc timer inherited from hosts/common.
nix.gc.automatic = lib.mkForce false;
services.netbird.enable = true;
environment.systemPackages = [pkgs.netbird-ui];
}
-30
View File
@@ -1,30 +0,0 @@
{
age = {
secrets = {
outline-key = {
file = ../../secrets/outline-key.age;
owner = "sascha.koenig";
};
ref-key = {
file = ../../secrets/ref-key.age;
owner = "sascha.koenig";
};
elevenlabs-key = {
file = ../../secrets/elevenlabs-key.age;
owner = "sascha.koenig";
};
exa-key = {
file = ../../secrets/exa-key.age;
owner = "sascha.koenig";
};
kestractl-env = {
file = ../../secrets/kestractl-env.age;
owner = "sascha.koenig";
};
"sascha.koenig-secrets" = {
file = ../../secrets/sascha.koenig-secrets.age;
owner = "sascha.koenig";
};
};
};
}
-114
View File
@@ -1,114 +0,0 @@
{
config,
pkgs,
...
}: {
environment.systemPackages = with pkgs; [
adcli # Helper library and tools for Active Directory client operations
oddjob # Odd Job Daemon
samba4Full # Standard Windows interoperability suite of programs for Linux and Unix
sssd # System Security Services Daemon
krb5 # MIT Kerberos 5
realmd # DBus service for configuring Kerberos and other
];
#
# Security
#
security = {
krb5 = {
enable = true;
settings = {
libdefaults = {
udp_preference_limit = 0;
default_realm = "AZ-GROUP";
};
};
};
pam = {
makeHomeDir.umask = "077";
services.login.makeHomeDir = true;
services.sshd.makeHomeDir = true;
};
sudo = {
extraConfig = ''
%domain\ admins ALL=(ALL:ALL) NOPASSWD: ALL
Defaults:%domain\ admins env_keep+=TERMINFO_DIRS
Defaults:%domain\ admins env_keep+=TERMINFO
'';
# Use extraConfig because of blank space in 'domain admins'.
# Alternatively, you can use the GID.
# extraRules = [
# { groups = [ "domain admins" ];
# commands = [ { command = "ALL"; options = [ "NOPASSWD" ]; } ]; }
# ];
};
};
#
# Services
#
services = {
nscd = {
enable = true;
config = ''
server-user nscd
enable-cache hosts yes
positive-time-to-live hosts 0
negative-time-to-live hosts 0
shared hosts yes
enable-cache passwd no
enable-cache group no
enable-cache netgroup no
enable-cache services no
'';
};
sssd = {
enable = true;
config = ''
[sssd]
domains = az-group
config_file_version = 2
services = nss, pam
[domain/az-group]
override_shell = /run/current-system/sw/bin/zsh
krb5_store_password_if_offline = True
cache_credentials = True
krb5_realm = AZ-GROUP
realmd_tags = manages-system joined-with-samba
id_provider = ad
fallback_homedir = /home/%u
ad_domain = your_domain_lowercase
use_fully_qualified_names = false
ldap_id_mapping = false
auth_provider = ad
access_provider = ad
chpass_provider = ad
ad_gpo_access_control = permissive
enumerate = true
'';
};
};
#
# Systemd
#
systemd = {
services.realmd = {
description = "Realm Discovery Service";
wantedBy = ["multi-user.target"];
after = ["network.target"];
serviceConfig = {
Type = "dbus";
BusName = "org.freedesktop.realmd";
ExecStart = "${pkgs.realmd}/libexec/realmd";
User = "root";
};
};
};
}
-44
View File
@@ -1,44 +0,0 @@
{pkgs, ...}: {
imports = [
# ./ad.nix
./greetd.nix
./mem0.nix
# ./n8n.nix
./netbird.nix
./printing.nix
./sound.nix
./udev.nix
];
services = {
espanso = {
enable = true;
package = pkgs.espanso-wayland;
};
hypridle.enable = true;
printing.enable = true;
gvfs.enable = true;
gnome.gnome-keyring.enable = true;
# qdrant = {
# enable = true;
# settings = {
# service = {
# host = "0.0.0.0";
# };
# };
# };
upower.enable = true;
avahi = {
enable = true;
nssmdns4 = true;
publish = {
addresses = true;
workstation = true;
userServices = true;
};
};
asusd = {
enable = true;
};
desktopManager.gnome.enable = true;
};
}
-37
View File
@@ -1,37 +0,0 @@
{
pkgs,
config,
lib,
...
}: let
tuigreet = "${lib.getExe pkgs.tuigreet}";
# Use start-hyprland wrapper to avoid Hyprland startup warnings
# withUWSM=true is set in programs.nix; start-hyprland handles this correctly
hyprlandCmd = "${config.programs.hyprland.package}/bin/start-hyprland";
in {
services.displayManager.gdm.enable = true;
services.greetd = {
enable = false;
settings = {
default_session = {
user = "greeter";
# Minimal config: verified supported flags only
# The --time and --remember are tested; power commands omitted
# to avoid potential quoting/parsing issues
command = builtins.concatStringsSep " " [
tuigreet
"--time"
"--remember"
"--asterisks"
"--cmd ${hyprlandCmd}"
];
};
};
};
# Required for --remember to persist username between logins
systemd.tmpfiles.rules = [
"d /var/cache/tuigreet 0755 greeter greeter - -"
];
}
-23
View File
@@ -1,23 +0,0 @@
{
m3ta.mem0 = {
enable = false;
port = 8000;
host = "127.0.0.1";
# LLM Configuration
llm = {
provider = "openai";
apiKeyFile = "/var/lib/mem0/openai-api-key-1"; # Use agenix or sops-nix
};
# Vector Storage Configuration
vectorStore = {
provider = "qdrant"; # or "chroma", "pinecone", etc.
config = {
host = "localhost";
port = 6333;
collection_name = "mem0_alice";
};
};
};
}
-14
View File
@@ -1,14 +0,0 @@
{...}: let
serviceName = "n8n";
in {
services.${serviceName} = {
enable = true;
openFirewall = true;
};
systemd.services.n8n = {
environment = {
N8N_SECURE_COOKIE = "false";
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS = "false";
};
};
}
-28
View File
@@ -1,28 +0,0 @@
{pkgs, ...}: {
services.netbird.enable = true;
systemd.services.netbird = {
environment = {
NB_DISABLE_SSH_CONFIG = "true";
};
path = [
pkgs.shadow
pkgs.util-linux
];
};
programs.ssh.extraConfig = ''
Match exec "${pkgs.netbird}/bin/netbird ssh detect %h %p"
PreferredAuthentications password,publickey,keyboard-interactive
PasswordAuthentication yes
PubkeyAuthentication yes
BatchMode no
ProxyCommand ${pkgs.netbird}/bin/netbird ssh proxy %h %p
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
CheckHostIP no
LogLevel ERROR
'';
networking.firewall.checkReversePath = "loose";
}
-70
View File
@@ -1,70 +0,0 @@
{pkgs, ...}: let
# Wait-Script für Erreichbarkeit des Druckers (IPPS-Port 443).
# Funktioniert sowohl im Office (direkte Route) als auch unterwegs via NetBird.
wait-for-printer = pkgs.writeShellScriptBin "wait-for-printer" ''
PRINTER_HOST="192.168.152.137"
PRINTER_PORT="443"
MAX_ATTEMPTS="120"
NC="${pkgs.netcat}/bin/nc"
for i in $(${pkgs.coreutils}/bin/seq 1 $MAX_ATTEMPTS); do
if $NC -z -w 1 "$PRINTER_HOST" "$PRINTER_PORT"; then
exit 0
fi
${pkgs.coreutils}/bin/sleep 1
done
echo "Printer $PRINTER_HOST:$PRINTER_PORT not reachable after ''${MAX_ATTEMPTS}s" >&2
exit 1
'';
in {
# CUPS Druckdienst für PDF-Druck aus n8n
# Drucker: Kyocera TASKalfa 4054ci @ 192.168.152.137
# Erreichbar im Office direkt oder unterwegs via NetBird-Tunnel.
# Das Wait-Script prüft IPPS-Port 443 – unabhängig vom Interface.
systemd.services.ensure-printers = {
wantedBy = ["multi-user.target"];
wants = ["netbird.service" "network-online.target"];
after = ["netbird.service" "network-online.target"];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStartPre = ["${wait-for-printer}/bin/wait-for-printer"];
Restart = "on-failure";
RestartSec = "30s";
};
};
services.printing = {
enable = true;
drivers = with pkgs; [
cups-filters # driverless IPP Everywhere Support
];
};
# Avahi für mDNS/IPP-Druckererkennung
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
# Kyocera TASKalfa 4054ci deklarativ einrichten
hardware.printers = {
ensurePrinters = [
{
name = "JW2OG";
location = "Buero";
description = "Kyocera TASKalfa 4054ci";
deviceUri = "ipps://192.168.152.137:443/ipp/print";
model = "everywhere";
ppdOptions = {
PageSize = "A4";
};
}
];
ensureDefaultPrinter = "JW2OG";
};
}
-11
View File
@@ -1,11 +0,0 @@
{
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = false;
wireplumber.enable = true;
};
}
-8
View File
@@ -1,8 +0,0 @@
{pkgs, ...}: {
services.udev.extraRules = ''
SUBSYSTEM=="usb", MODE="0666"
'';
environment.systemPackages = with pkgs; [
zsa-udev-rules
];
}
-26
View File
@@ -1,26 +0,0 @@
# hosts/AZ-TC-01/default.nix
#
# Fleet Host AZ-TC-01 — pilot #1 of the AZ-NIX-CLIENTS Thin Client fleet.
# Imports the thin-client role, declares hardware class, and that's it.
# Everything else is composed by roles/thin-client/default.nix.
{...}: {
imports = [
../../roles/thin-client
];
az.tc = {
enable = true;
hardwareClass = "dell-optiplex-micro";
# Staging mode silences placeholder-warnings (real SSID, hotline,
# company name) until we have the final corp values.
site = "staging";
};
networking.hostName = "AZ-TC-01";
# Disk device override for this specific box (Dell OptiPlex 3040 Micro
# with a 256GB SATA SSD).
az.tc.deployment.diskDevice = "/dev/nvme0n1";
system.stateVersion = "25.05";
}
-16
View File
@@ -1,16 +0,0 @@
# hosts/AZ-TC-02/default.nix
# Fleet Host AZ-TC-02 — pilot #2.
{...}: {
imports = [../../roles/thin-client];
az.tc = {
enable = true;
hardwareClass = "dell-optiplex-micro";
site = "staging";
};
networking.hostName = "AZ-TC-02";
az.tc.deployment.diskDevice = "/dev/sda";
system.stateVersion = "25.05";
}
-16
View File
@@ -1,16 +0,0 @@
# hosts/AZ-TC-03/default.nix
# Fleet Host AZ-TC-03 — pilot #3.
{...}: {
imports = [../../roles/thin-client];
az.tc = {
enable = true;
hardwareClass = "dell-optiplex-micro";
site = "staging";
};
networking.hostName = "AZ-TC-03";
az.tc.deployment.diskDevice = "/dev/sda";
system.stateVersion = "25.05";
}
-80
View File
@@ -1,80 +0,0 @@
# Common configuration for all hosts
{
config,
pkgs,
lib,
inputs,
outputs,
system,
...
}: {
imports = [
./extraServices
./ports.nix
./users
inputs.home-manager.nixosModules.home-manager
];
environment.pathsToLink = ["/share/xdg-desktop-portal" "/share/applications"];
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
extraSpecialArgs = {
inherit inputs outputs system;
videoDrivers = config.services.xserver.videoDrivers or [];
};
};
nixpkgs = {
# You can add overlays here
overlays = [
# Add overlays your own flake exports (from overlays and pkgs dir):
#outputs.overlays.additions
outputs.overlays.modifications
outputs.overlays.stable-packages
# outputs.overlays.pinned-packages
inputs.m3ta-nixpkgs.overlays.default
inputs.m3ta-nixpkgs.overlays.modifications
(outputs.lib.mkLlmAgentsOverlay system)
# You can also add overlays exported from other flakes:
# neovim-nightly-overlay.overlays.default
# Or define it inline, for example:
# (final: prev: {
# hi = final.hello.overrideAttrs (oldAttrs: {
# patches = [ ./change-hello-to-hi.patch ];
# });
# })
];
# Configure your nixpkgs instance
config = {
# Disable if you don't want unfree packages
allowUnfree = true;
};
};
nix = {
settings = {
experimental-features = "nix-command flakes";
cores = 2;
max-jobs = 8;
trusted-users = [
"root"
"sascha.koenig"
]; # Set users that are allowed to use the flake command
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
optimise.automatic = true;
registry =
(lib.mapAttrs (_: flake: {inherit flake;}))
((lib.filterAttrs (_: lib.isType "flake")) inputs);
nixPath = ["/etc/nix/path"];
};
users.defaultUserShell = pkgs.nushell;
}
-8
View File
@@ -1,8 +0,0 @@
{
imports = [
./flatpak.nix
./ollama.nix
./podman.nix
./virtualisation.nix
];
}
-23
View File
@@ -1,23 +0,0 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.extraServices.flatpak;
in {
options.extraServices.flatpak.enable = mkEnableOption "enable flatpak";
config = mkIf cfg.enable {
services.flatpak.enable = true;
xdg.portal = {
# xdg desktop intergration (required for flatpak)
enable = true;
extraPortals = with pkgs; [
xdg-desktop-portal-hyprland
];
config.common.default = "*";
};
};
}
-27
View File
@@ -1,27 +0,0 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.extraServices.ollama;
in {
options.extraServices.ollama.enable = mkEnableOption "enable ollama";
config = mkIf cfg.enable {
services.ollama = {
enable = true;
package = pkgs.ollama-vulkan;
host = "[::]";
openFirewall = true;
environmentVariables = {
OLLAMA_HOST = "0.0.0.0";
};
};
nixpkgs.config = {
rocmSupport = config.services.xserver.videoDrivers == ["amdgpu"];
cudaSupport = config.services.xserver.videoDrivers == ["nvidia"];
};
};
}
-33
View File
@@ -1,33 +0,0 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.extraServices.podman;
in {
options.extraServices.podman.enable = mkEnableOption "enable podman";
config = mkIf cfg.enable {
virtualisation = {
podman = {
enable = true;
dockerCompat = true;
dockerSocket.enable = true;
autoPrune = {
enable = true;
dates = "weekly";
flags = [
"--filter=until=24h"
"--filter=label!=important"
];
};
defaultNetwork.settings.dns_enabled = true;
};
};
environment.systemPackages = with pkgs; [
podman-compose
];
};
}
@@ -1,42 +0,0 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.extraServices.virtualisation;
in {
options.extraServices.virtualisation.enable = mkEnableOption "enable virtualisation";
config = mkIf cfg.enable {
virtualisation = {
libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
runAsRoot = true;
swtpm.enable = true;
};
};
};
programs.virt-manager.enable = true;
environment = {
systemPackages = [pkgs.qemu];
etc = {
"ovmf/OVMF_CODE.fd" = {
source = "${(pkgs.OVMF.override {
secureBoot = true;
tpmSupport = true;
}).fd}/FV/OVMF_CODE.fd";
};
"ovmf/OVMF_VARS.fd" = {
source = "${(pkgs.OVMF.override {
secureBoot = true;
tpmSupport = true;
}).fd}/FV/OVMF_VARS.fd";
};
};
};
};
}
-31
View File
@@ -1,31 +0,0 @@
{config, ...}: {
m3ta.ports = {
enable = true;
definitions = {
# System services
ssh = 2022;
# Web & proxy services
traefik = 80;
traefik-ssl = 443;
# Databases
postgres = 5432;
mysql = 3306;
redis = 6379;
};
hostOverrides = {
# Host-specific overrides
AZ-LT-NIX = {
# Any custom port overrides for m3-ares
};
};
};
environment.etc."info/all-ports.json" = {
text = builtins.toJSON {
hostname = config.networking.hostName;
ports = config.m3ta.ports.all; # TODO should only return actually used ports
};
};
}
-6
View File
@@ -1,6 +0,0 @@
{
imports = [
./jannik.mueller.nix
./sascha.koenig.nix
];
}
-25
View File
@@ -1,25 +0,0 @@
{
config,
pkgs,
inputs,
...
}: {
users.users."jannik.mueller" = {
hashedPassword = "$y$j9T$09RgD3AU3PK9Oi6JGLe0V1$i8J2ZOD1h1b6Zpw28ub.kExujoDKHzokeXzkM23Tfd/";
isNormalUser = true;
extraGroups = [
"wheel"
"networkmanager"
"libvirtd"
"flatpak"
"plugdev"
"input"
"kvm"
"qemu-libvirtd"
];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPvZazSuIoWoRWhkAqQDMLeurxVUyy1MTllp1wfw1tzq"
];
packages = [inputs.home-manager.packages.${pkgs.stdenv.hostPlatform.system}.default];
};
}
-158
View File
@@ -1,158 +0,0 @@
# hosts/common/users/m3tam3re.nix — Central user definition with m3ta-home integration.
#
# This module:
# 1. Creates the m3tam3re NixOS user
# 2. Loads the m3ta-home profile system via mkHome
# 3. Sets per-host feature flags based on a host profile mapping
# 4. Imports per-host home.nix overrides (monitors, HW-specific config)
#
# To add a new host:
# 1. Add entry to hostProfiles below
# 2. Add feature flags in the hostFlags section
# 3. Create hosts/<hostname>/home.nix if the host needs overrides (monitors, etc.)
{
config,
pkgs,
inputs,
...
}: let
hostname = config.networking.hostName;
# ── Per-host profile mapping ──
# Determines which m3ta-home context and sets each host gets.
hostProfiles = {
# ── Desktop hosts ──
AZ-LT-NIX = {
context = "desktop";
sets = ["coding" "media"];
};
};
profile =
hostProfiles.${
hostname
} or {
context = "server";
sets = [];
};
m3ta-lib = inputs.m3ta-home.lib;
# Check if a per-host home.nix exists
hostHomeFile = ./../../${hostname}/home.nix;
hostHomeExists = builtins.pathExists hostHomeFile;
# ── Per-host feature flags ──
# These enable/disable specific m3ta-home modules per host.
hostFlags =
if hostname == "AZ-LT-NIX"
then {
# Full desktop workstation
base = {
shell = {
fish.enable = true;
nushell.enable = true;
starship.enable = true;
};
cliTools = {
fzf.enable = true;
nitch.enable = true;
television.enable = true;
};
secrets.enable = true;
};
desktop = {
wm = {
hyprland.enable = true;
rofi.enable = true;
wayland.enable = true;
dms.enable = true;
};
apps = {
crypto.enable = false;
obsidian.enable = true;
office.enable = true;
};
theme = {
fonts.enable = true;
wallpapers.enable = true;
};
};
coding = {
editors = {
neovim.enable = true;
zed.enable = true;
};
lsp.enable = true;
packages.enable = true;
languages = {
python.enable = true;
javascript.enable = true;
rustToolchain.enable = true;
go.enable = true;
typescript.enable = true;
};
};
profiles.media = {
obs.enable = true;
ffmpeg.enable = true;
kdenlive.enable = true;
ytDlp.enable = true;
};
}
else {
# m3-helios, m3-hermes, m3-aether — minimal server
base = {
shell = {
fish.enable = true;
starship.enable = true;
};
cliTools = {
fzf.enable = true;
nitch.enable = true;
};
};
};
in {
# ── NixOS user definition ──
users.users."sascha.koenig" = {
hashedPassword = "$y$j9T$ORX4btVZgs9Xjq2oIvzJm0$lXiPwaa0D6t.eMDIx1UBesEAMOkWXBoGwpeI7X0aS8D";
isNormalUser = true;
shell = pkgs.nushell;
extraGroups = [
"wheel"
"networkmanager"
"libvirtd"
"flatpak"
"plugdev"
"input"
"kvm"
"qemu-libvirtd"
];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZbg/Z9mnflXuLahGY8WOSBMqbgeqVIkIwRkquys1Ml sascha.koenig@azintec.com"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3YEmpYbM+cpmyD10tzNRHEn526Z3LJOzYpWEKdJg8DaYyPbDn9iyVX30Nja2SrW4Wadws0Y8DW+Urs25/wVB6mKl7jgPJVkMi5hfobu3XAz8gwSdjDzRSWJrhjynuaXiTtRYED2INbvjLuxx3X8coNwMw58OuUuw5kNJp5aS2qFmHEYQErQsGT4MNqESe3jvTP27Z5pSneBj45LmGK+RcaSnJe7hG+KRtjuhjI7RdzMeDCX73SfUsal+rHeuEw/mmjYmiIItXhFTDn8ZvVwpBKv7xsJG90DkaX2vaTk0wgJdMnpVIuIRBa4EkmMWOQ3bMLGkLQeK/4FUkNcvQ/4+zcZsg4cY9Q7Fj55DD41hAUdF6SYODtn5qMPsTCnJz44glHt/oseKXMSd556NIw2HOvihbJW7Rwl4OEjGaO/dF4nUw4c9tHWmMn9dLslAVpUuZOb7ykgP0jk79ldT3Dv+2Hj0CdAWT2cJAdFX58KQ9jUPT3tBnObSF1lGMI7t77VU= m3tam3re@MBP-Sascha.fritz.box"
];
packages = [inputs.home-manager.packages.${pkgs.stdenv.hostPlatform.system}.default];
};
# ── Home-Manager configuration via m3ta-home ──
home-manager.users."sascha.koenig" = {
imports =
[
# Load m3ta-home composition engine
(m3ta-lib.mkHome {
user = "m3tam3re";
identity = "work";
inherit (profile) context sets;
})
# Per-host feature flags
hostFlags
]
# Per-host home.nix (Hyprland monitors, XDG/MIME, HW-specific overrides)
++ (
if hostHomeExists
then [hostHomeFile]
else []
);
};
}
-49
View File
@@ -1,49 +0,0 @@
{inputs, ...}: {
# This one brings our custom packages from the 'pkgs' directory
# additions = final: prev:
# (import ../pkgs {pkgs = final;})
# // {
# zugferd-service = inputs.zugferd-service.packages.${prev.stdenv.hostPlatform.system}.default;
# };
# This one contains whatever you want to overlay
# You can change versions, add patches, set compilation flags, anything really.
# https://nixos.wiki/wiki/Overlays
modifications = final: prev: {
# n8n = import ./mods/n8n.nix {inherit prev;};
font-manager = import ./mods/font-manager.nix {inherit prev;};
vivaldi = prev.vivaldi.override {
commandLineArgs = "--enable-features=UseOzonePlatform --ozone-platform=wayland";
};
# example = prev.example.overrideAttrs (oldAttrs: rec {
# ...
# });
};
stable-packages = final: _prev: {
stable = import inputs.nixpkgs-stable {
system = final.stdenv.hostPlatform.system;
config.allowUnfree = true;
};
};
mkLlmAgentsOverlay = system: _final: _prev:
removeAttrs (inputs.llm-agents.packages.${system} or {}) [
# Internal helpers from packages/* marked hideFromDocs upstream:
"antigravity"
"auto-claude"
"buildNpmPackage" # ← the culprit; breaks webcord's `buildNpmPackage.override { nodejs = ...; }`
"bun2nix"
"darwinOpenptyHook"
"default" # fzf launcher; collides with nothing in nixpkgs but kept out for hygiene
"dolt" # collides with nixpkgs.dolt (Dolt database); not used in this config
"flake-inputs"
"forge"
"formatelf"
"formatter" # collides conceptually with pkgs.formatter; not used here
"go-bin"
"unpinCargoMsrvHook"
"unpinGoModVersionHook"
"versionCheckHomeHook"
"wrapBuddy"
];
}
-20
View File
@@ -1,20 +0,0 @@
# font-manager 0.9.4: fix compilation against gtk4 >= 4.22 / newer Vala.
#
# In GTK 4.22 the Gtk.DragIcon.get_for_drag binding changed in the gtk4
# vapi, turning `Gtk.DragIcon.get_for_drag(drag)` into a hard Vala error
# ("use `new' operator to create new objects") in Collections.vala and
# FontList.vala. 0.9.4 is the latest upstream release and even upstream
# master is unfixed; the fix lives in FontManager/font-manager#468 and is
# already carried as a patch by current nixpkgs master — but our locked
# nixpkgs-unstable predates it.
#
# Vendored from m3tam3re/nixos-config overlays/mods (2026-08-31).
# Drop this overlay (and the patch file) once our nixpkgs lock advances
# past nixpkgs@e2ad529-vendoring (pkgs/by-name/fo/font-manager carries
# fix-compilation-error-with-newer-vala-versions.patch).
{prev}:
prev.font-manager.overrideAttrs (old: {
patches =
(old.patches or [])
++ [./patches/font-manager-fix-compilation-error-with-newer-vala-versions.patch];
})
-26
View File
@@ -1,26 +0,0 @@
# {prev}:
# prev.n8n.overrideAttrs (oldAttrs: rec {
# version = "1.112.6";
# src = prev.fetchFromGitHub {
# owner = "n8n-io";
# repo = "n8n";
# rev = "n8n@${version}";
# hash = "sha256-r/MCU/S1kkKQPkhmp9ZHTtgZxMu5TFCl5Yejp73gATw=";
# };
# pnpmDeps = prev.pnpm_10.fetchDeps {
# pname = oldAttrs.pname;
# inherit version src;
# fetcherVersion = 1;
# hash = "sha256-j+HJhvzrcu8JsezcFJxfgteOgTspWQb2ZSN2fEl7Voo=";
# };
# nativeBuildInputs =
# builtins.map
# (input:
# if input == prev.pnpm_9.configHook
# then prev.pnpm_10.configHook
# else input)
# oldAttrs.nativeBuildInputs;
# })
@@ -1,37 +0,0 @@
From e2ad529a88929bbc76906ac78260dacf4d8c8c6b Mon Sep 17 00:00:00 2001
From: Jan Baier <jan.baier@amagical.net>
Date: Fri, 1 May 2026 17:25:25 +0200
Subject: [PATCH] Fix compilation error with newer Vala versions
Fixes #467
---
src/font-manager/Collections.vala | 2 +-
src/font-manager/FontList.vala | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/font-manager/Collections.vala b/src/font-manager/Collections.vala
index 5e7f5cdb..43531e54 100644
--- a/src/font-manager/Collections.vala
+++ b/src/font-manager/Collections.vala
@@ -523,7 +523,7 @@ namespace FontManager {
var row = ((CollectionListRow) source.widget);
var drag_icon = new Gtk.Label(row.item_label.label);
drag_icon.add_css_class("FontManagerListRowDrag");
- var gtk_drag_icon = (Gtk.DragIcon) Gtk.DragIcon.get_for_drag(drag);
+ var gtk_drag_icon = new Gtk.DragIcon.get_for_drag(drag);
gtk_drag_icon.set_child(drag_icon);
return;
}
diff --git a/src/font-manager/FontList.vala b/src/font-manager/FontList.vala
index c8b170cb..9720dc2b 100644
--- a/src/font-manager/FontList.vala
+++ b/src/font-manager/FontList.vala
@@ -669,7 +669,7 @@ namespace FontManager {
widget_set_name(drag_count, "FontManagerListDragCount");
drag_icon.add_overlay(drag_count);
drag_count.set_label(selected_items.length.to_string());
- var gtk_drag_icon = (Gtk.DragIcon) Gtk.DragIcon.get_for_drag(drag);
+ var gtk_drag_icon = new Gtk.DragIcon.get_for_drag(drag);
gtk_drag_icon.set_child(drag_icon);
return;
}
-4
View File
@@ -1,4 +0,0 @@
{pkgs, ...}: {
# Define your custom packages here
# pomodoro-timer = pkgs.callPackage ./pomodoro-timer {};
}
-253
View File
@@ -1,253 +0,0 @@
# roles/thin-client — AZ-NIX-CLIENTS Thin Client Fleet
NixOS role for the `AZ-TC-NN` fleet. Replaces a Windows 10 workstation
with a locked-down NixOS + KDE Plasma client that authenticates against
Active Directory and provides pre-configured RDP, browser, and Office-Web
access.
This document describes the provisioning workflow for a new Fleet Host.
## Decision summary
All 20 design decisions from the grilling session are captured in the
top-level `default.nix` comments and the individual submodule headers.
Quick reference:
| Area | Decision |
| --- | --- |
| Hardware | Dell OptiPlex Micro (3020/3040/5040/7040) + generic-x86_64-uefi fallback |
| Deployment | Stock Linux ISO + `nixos-anywhere` |
| Disk | BTRFS (`@root`, `@home`, `@nix`, `@persist`, `@snapshots`), zstd |
| AD | Realm `AZ-GROUP` / DNS `az-group.local`, pre-created computer accounts + keytab via agenix |
| Shell | bash for domain users |
| Session | KDE Plasma 6, Wayland, SDDM, no autologin |
| Sudo | `%domain admins` with password; local `sascha.koenig` + `jannik.mueller` as break-glass |
| WiFi | 802.1X EAP-TLS per-machine, NetworkManager + wpa_supplicant (not iwd) |
| SMB | pam_mount, Kerberos, DFS namespace, lax offline |
| RDP | System-wide Remmina, Kerberos SSO, fullscreen multi-monitor, audio local only |
| Office | 5 .desktop shortcuts via `chromium --app=URL`; Outlook + Teams autostart |
| Chromium | ManagedBookmarks, sync disabled, uBlock Origin + Bitwarden, no local passwords |
| RustDesk | Client + daemon, self-hosted server, pre-shared key, Wayland portal pre-authorized |
| OBS | Pre-configured "AZ-Default" profile, output to `~/Videos/OBS/` |
| NetBird | Per-host setup key, corp DNS + NetBird DNS parallel, accept-routes |
| Printers | Single Pull-Print queue, direct IPPS, Avahi off |
| Branding | Light: corporate wallpaper + SDDM logo + property footer, no banner |
| Updates | `system.autoUpgrade` daily at 03:00, reboot window 03:00-05:00 |
| Monitoring | node_exporter → Pushgateway, Alloy → Loki (journal), Snipe-IT asset check-in (daily 03:30) |
| Rollout | Pilot: 3 hosts, 2 weeks; then 5 → 10 → rest |
## Provisioning workflow for a new Fleet Host
Assuming you've copied `hosts/AZ-TC-01/default.nix` to
`hosts/AZ-TC-NN/default.nix`, updated `networking.hostName`, added the
flake entry, and committed.
### Step 1 — Pre-create the AD computer account + keytab
Run from a host that can reach the AD DC (e.g. `AZ-LT-NIX`). Requires
`adcli` (available on the admin workstation via `nix-shell -p adcli` if
not installed).
Prerequisite: the target OU must exist in AD. If `OU=ThinClients,...`
doesn't exist yet, create it first (or omit `--domain-ou` to use the
default `CN=Computers,...` container).
```bash
# Pre-create the computer object in the ThinClients OU
adcli preset-computer \
--domain=az-group.local \
--domain-ou="OU=ThinClients,DC=az-group,DC=local" \
--os-name="NixOS" --os-version="26.05" \
--login-user=administrator \
AZ-TC-01.az-group.local
# Produce a keytab for that pre-created computer object.
# --host-fqdn redirects adcli from the local machine to the AZ-TC-01
# account; -K writes only to the specified path (does NOT modify the
# local machine's /etc/krb5.keytab).
adcli join \
--domain=az-group.local \
--host-fqdn=AZ-TC-01.az-group.local \
--os-name="NixOS" --os-version="26.05" \
--login-user=administrator \
-K /tmp/AZ-TC-01.keytab
# Verify the keytab has expected principals
klist -k /tmp/AZ-TC-01.keytab
# Expected principals:
# AZ-TC-01$@AZ-GROUP
# host/AZ-TC-01.az-group.local@AZ-GROUP
# RestrictedKrbHost/AZ-TC-01.az-group.local@AZ-GROUP
# HOST/AZ-TC-01@AZ-GROUP
# RestrictedKrbHost/AZ-TC-01@AZ-GROUP
# Provision the agenix secret
agenix -e secrets/AZ-TC-01-krb5-keytab.age
# In the editor: paste /tmp/AZ-TC-01.keytab contents, save, exit.
rm /tmp/AZ-TC-01.keytab
```
Tip: if you already have an admin TGT (via `kinit administrator@AZ-GROUP`),
you can replace `--login-user=administrator` with
`--login-ccache=${KRB5CCNAME:-/tmp/krb5cc_$(id -u)}` to skip the password
prompt.
### Step 2 — Provision NetBird setup key
In the NetBird UI (`https://netbird.az-group.local`):
1. Go to **Setup Keys** → **Generate new key**.
2. Name: `AZ-TC-01`, Type: **one-time** (or reusable for fleet), Expires: 90d.
3. Copy the key value.
4. `agenix -e secrets/AZ-TC-01-netbird-setupkey.age` → paste the key.
### Step 3 — Provision WiFi client certificate
In AD CS (`https://certsrv.az-group.local/certsrv`):
1. **Request a Certificate** → **Advanced certificate request**.
2. Template: **Workstation Authentication** (or your corp 802.1X template).
3. Subject: `CN=AZ-TC-01$`.
4. Export the cert + private key as PKCS#12 (`.pfx`), with exportable key.
5. Convert to combined PEM (cert + key in one file):
```bash
openssl pkcs12 -in AZ-TC-01.pfx -out AZ-TC-01.pem -nodes
```
6. `agenix -e secrets/AZ-TC-01-wifi-client-cert.age` → paste the PEM.
### Step 4 — Provision RustDesk credentials
```bash
# Generate a strong permanent password per host
PW=$(openssl rand -base64 24)
echo -n "$PW" > /tmp/AZ-TC-01-rustdesk-pw.txt
agenix -e secrets/AZ-TC-01-rustdesk-password.age
# Paste /tmp/AZ-TC-01-rustdesk-pw.txt contents.
rm /tmp/AZ-TC-01-rustdesk-pw.txt
```
The shared `rustdesk-psk.age` is one-time setup, reused for all hosts.
### Step 5 — Provision the host
Boot the OptiPlex from a stock Linux ISO (any modern NixOS installer ISO
or Ubuntu live ISO works). From the admin workstation:
```bash
# Bootstrap nixos-anywhere into the booted ISO (via SSH or physical console)
# Then run the install:
nixos-anywhere \
--flake .#AZ-TC-01 \
--disko-config ./roles/thin-client/deployment/disko.nix \
root@<target-ip>
```
The host will:
1. Partition the disk via disko (BTRFS layout).
2. Install NixOS from the flake.
3. Reboot into the new system.
4. On first boot: agenix decrypts secrets, sssd starts with keytab,
NetBird enrolls via setup key, WiFi connects via EAP-TLS.
### Step 6 — Capture the host's SSH host key
After first boot:
```bash
ssh-keyscan -t ed25519 AZ-TC-01.netbird | awk '{print $2 " " $3}'
```
Update `secrets.nix` with the real `AZ-TC-01` SSH key value (replacing
the `PLACEHOLDER` line). Rebuild and redeploy.
## Option reference
All tunable parameters live under `az.tc.*`:
| Option | Default | Description |
| --- | --- | --- |
| `az.tc.enable` | `false` | Activate the thin-client role |
| `az.tc.hardwareClass` | `generic-x86_64-uefi` | One of `dell-optiplex-micro` or `generic-x86_64-uefi` |
| `az.tc.site` | `default` | Site identifier (informational) |
| `az.tc.ad.ou` | `OU=ThinClients,DC=az-group,DC=local` | AD Organizational Unit for computer objects |
| `az.tc.wifi.ssid` | `AZ-CORP` | Corp WiFi SSID |
| `az.tc.wifi.caCert` | `./assets/corp-wifi-ca.pem` | Corp CA cert (NOT secret) |
| `az.tc.netbird.managementUrl` | `https://netbird.az-group.local:443` | NetBird server URL |
| `az.tc.smb.dfsNamespace` | `\\\\az-group.local\\dfs` | DFS namespace root |
| `az.tc.smb.userShare` | `users/%u` | Per-user share path |
| `az.tc.smb.commonShares` | `[public software]` | Common share names |
| `az.tc.smb.mountRoot` | `/mnt/az-dfs` | Local mount root |
| `az.tc.printing.pullPrintEndpoint` | `ipps://pull-print.az-group.local:443/ipp/print` | Pull-Print IPPS URI |
| `az.tc.printing.queueName` | `Pull-Print` | Local CUPS queue name |
| `az.tc.rdp.servers` | `[{name="TS Berlin"; fqdn="ts-berlin.az-group.local";}]` | TS endpoints |
| `az.tc.chromium.homepage` | `https://www.office.com` | Browser homepage |
| `az.tc.chromium.bookmarks` | `[Outlook, Teams, SharePoint, Office]` | Managed bookmarks |
| `az.tc.chromium.bitwardenServerUrl` | `https://vault.bitwarden.com` | Bitwarden URL |
| `az.tc.rustdesk.serverHost` | `rustdesk.az-group.local` | RustDesk server host |
| `az.tc.branding.hotline` | `+49 30 1234567` | IT hotline for footer |
| `az.tc.branding.wallpaper` | `./assets/wallpaper.svg` | Wallpaper path |
| `az.tc.branding.logo` | `./assets/logo.svg` | Logo path |
| `az.tc.branding.company` | `AzIntec GmbH` | Company name |
| `az.tc.monitoring.prometheusPushGateway` | `pushgateway.az-group.local:9091` | Pushgateway URL |
| `az.tc.monitoring.lokiUrl` | `http://loki.az-group.local:3100` | Loki URL |
| `az.tc.monitoring.snipeItUrl` | `https://snipeit.az-group.local` | Snipe-IT base URL |
| `az.tc.monitoring.assetTool` | `snipe-it` | Asset tool integration |
| `az.tc.deployment.diskDevice` | `/dev/sda` | Disko target disk |
| `az.tc.deployment.swapSizeGB` | `4` | Swap size in GB |
## Open items before pilot deploy
These placeholders need real values before pilot deploy. They're flagged
with `# TODO:` in the respective modules.
- [ ] Real corp WiFi SSID
- [ ] Real CA root cert (replace `corp-wifi-ca.pem`)
- [ ] Real DFS namespace path
- [ ] Real common share names
- [ ] Real Terminal Server endpoints
- [ ] Real Chromium bookmarks (Intranet, Helpdesk, ERP, HR)
- [ ] Real Bitwarden server URL (if self-hosted)
- [ ] Real RustDesk server hostname
- [ ] Real Pull-Print IPPS endpoint
- [ ] Real NetBird management URL
- [ ] Real Prometheus Pushgateway URL
- [ ] Real Loki URL
- [ ] Real IT-Hotline phone number
- [ ] Real corporate wallpaper asset (replace placeholder SVG)
- [ ] Real corporate logo asset (replace placeholder SVG)
- [ ] Real AD DCs (`az-dc01.az-group.local`, `az-dc02.az-group.local` — currently guessed)
- [ ] Real AD Computer OU DN
## Build validation
To validate the config without deploying:
```bash
# Eval-check all hosts
nix flake check
# Build a pilot host closure (full closure, takes ~20 min uncached)
nix build --no-link .#nixosConfigurations.AZ-TC-01.config.system.build.toplevel
# Try a VM boot
nix run .#nixosConfigurations.AZ-TC-01.config.system.build.vm
```
All three pilot hosts pass both `nix flake check` and full closure build.
## Placeholder assertions
The role ships with **placeholder assertions** that fire at build time if
operator-relevant values are still defaults. To silence for lab/staging
hosts, set `az.tc.site = "staging"` (see `hosts/AZ-TC-01/default.nix`).
Currently asserted:
- `az.tc.wifi.ssid != "AZ-CORP"` — real Corp SSID must be set
- `az.tc.branding.hotline != "+49 30 1234567"` — real hotline must be set
- `az.tc.branding.company != "AzIntec GmbH"` — real company name must be set
Add more assertions as customer-specific values firm up.
-42
View File
@@ -1,42 +0,0 @@
# roles/thin-client/apps/autostart.nix
#
# KDE autostart for Outlook + Teams at login.
# Q11 decision: "Outlook + Teams beim Login".
#
# Implemented as system-wide .desktop files in
# /etc/xdg/autostart/ — KDE's Plasma session picks them up automatically
# for every user.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
mkAutostart = name: url: let
filename = builtins.replaceStrings [" "] ["-"] (lib.toLower name);
in
pkgs.writeTextFile {
name = "${filename}-autostart.desktop";
destination = "/etc/xdg/autostart/${filename}-autostart.desktop";
text = ''
[Desktop Entry]
Type=Application
Name=${name} (Auto-Start)
Exec=${pkgs.chromium}/bin/chromium --app=${url} --no-default-browser-check --no-first-run
Icon=web-browser
Terminal=false
X-KDE-autostart-after=panel
X-KDE-autostart-phase=2
X-GNOME-Autostart-enabled=true
'';
};
in {
config = mkIf cfg.enable {
environment.etc = {
"xdg/autostart/outlook-autostart.desktop".source =
(mkAutostart "Outlook" "https://outlook.office.com")
+ "/etc/xdg/autostart/outlook-autostart.desktop";
"xdg/autostart/teams-autostart.desktop".source =
(mkAutostart "Teams" "https://teams.microsoft.com")
+ "/etc/xdg/autostart/teams-autostart.desktop";
};
};
}
-119
View File
@@ -1,119 +0,0 @@
# roles/thin-client/apps/chromium.nix
#
# Chromium enterprise policy.
# Q12 decisions: ManagedBookmarks, SyncDisabled, uBlock Origin + Allowlist,
# PasswordManagerEnabled=false, Bitwarden Extension force-install.
#
# Policy file path on NixOS: /etc/chromium/policies/managed/*.json
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
in {
options.az.tc.chromium = {
homepage = mkOption {
type = types.str;
default = "https://www.office.com"; # TODO: real homepage
description = "Chromium startup URL.";
};
bookmarks = mkOption {
type = types.listOf (types.submodule {
options = {
name = mkOption {type = types.str;};
url = mkOption {type = types.str;};
};
});
default = [
{name = "Outlook Web"; url = "https://outlook.office.com";}
{name = "Teams Web"; url = "https://teams.microsoft.com";}
{name = "SharePoint"; url = "https://azgroup.sharepoint.com";}
{name = "Office Home"; url = "https://www.office.com";}
# TODO: Intranet, Helpdesk, ERP, HR — fill real URLs
];
description = "Managed bookmarks (cannot be deleted by user).";
};
bitwardenServerUrl = mkOption {
type = types.str;
default = "https://vault.bitwarden.com"; # cloud default; override for self-hosted
description = ''
Bitwarden server URL (cloud or self-hosted). Pushed via Chromium
extension settings.
'';
};
};
config = mkIf cfg.enable {
programs.chromium = {
enable = true;
extraOpts = {
# Bookmarks
ManagedBookmarks = [
{cn = "AZ Corporate Bookmarks"; children = config.az.tc.chromium.bookmarks;}
];
# Homepage
HomepageLocation = config.az.tc.chromium.homepage;
RestoreOnStartup = 4; # always restore homepage
StartupUrls = [config.az.tc.chromium.homepage];
# Sync disabled (Q12)
SyncDisabled = true;
BrowserSignin = 0; # no Google account sign-in
RestrictSigninToPattern = ".*@az-group\\.local";
# Password manager disabled (Q12)
PasswordManagerEnabled = false;
# Autofill
AutofillAddressEnabled = true;
AutofillCreditCardEnabled = false;
# Background mode (Chromium keeps running in background)
BackgroundModeEnabled = false;
# Extensions: force-install uBlock Origin + Bitwarden
ExtensionInstallForcelist = [
# uBlock Origin
"cjpalhdlnbpafiamejdnhcphjbkeiagm"
# Bitwarden Password Manager
"nngceckbapebfimnlniiiahkandclblb"
];
ExtensionInstallAllowlist = [
"cjpalhdlnbpafiamejdnhcphjbkeiagm" # uBlock Origin
"nngceckbapebfimnlniiiahkandclblb" # Bitwarden
];
ExtensionInstallBlocklist = ["*"]; # block everything not in allowlist
# Bitwarden configuration via extension policy
"3rdparty" = {
"extensions" = {
"nngceckbapebfimnlniiiahkandclblb" = {
environment = {
base = config.az.tc.chromium.bitwardenServerUrl;
};
};
};
};
# Disable telemetry
UrlKeyedAnonymizedDataCollectionEnabled = false;
SafeBrowsingEnabled = true;
# Default browser check
DefaultBrowserSettingEnabled = true;
# PDF / plugins / popups
AlwaysOpenPdfExternally = false;
BlockThirdPartyCookies = true;
# Pinch-to-zoom, etc.
TouchEnabled = false;
};
};
environment.systemPackages = with pkgs; [
chromium
];
};
}
-14
View File
@@ -1,14 +0,0 @@
# roles/thin-client/apps/default.nix
#
# All user-facing applications: Chromium, Office-Web shortcuts, Remmina
# (RDP), RustDesk, OBS, and login-autostart for Outlook + Teams.
{config, lib, ...}: {
imports = [
./chromium.nix
./office-web.nix
./remmina.nix
./rustdesk.nix
./obs.nix
./autostart.nix
];
}
-85
View File
@@ -1,85 +0,0 @@
# roles/thin-client/apps/obs.nix
#
# OBS Studio for Schulungs-Recording. Pre-configured profile with
# PipeWire screen capture + Mic + System-Audio, Output to ~/Videos/OBS/.
# Q14 decisions: Use Case Schulungs-Recording, Pre-configuriertes Profil,
# Output local, no streaming.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
environment = {
systemPackages = with pkgs; [
obs-studio
obs-studio-plugins.obs-pipewire-audio-capture
obs-studio-plugins.wlrobs
];
etc = {
# System-wide OBS profile "AZ-Default".
# Installed to /etc/obs/az-default/ and copied to user profile on
# first run.
"obs/az-default/basic.ini".text = ''
[General]
Name=AZ-Default
[Output]
Mode=Simple
FilenameFormatting=%CCYY-%MM-%DD %hh-%mm-%ss
DelayEnable=false
DelaySec=20
DelayPreserve=false
ReplayWhileRecording=false
ReplayBufferEnable=false
[SimpleOutput]
FilePath=/home/%/Videos/OBS/
RecFormat=mp4
VEncoder=x264
VBitrate=6000
AEncoder=aac
ABitrate=192
[Audio]
SampleRate=48000
ChannelSetup=stereo
[Video]
Base=1920x1080
Output=1920x1080
FPSType=0
FPSCommon=30
[AdvOut]
ApplyServiceSettings=true
RescaleRes=1920x1080
'';
# Pre-configured scene collection with PipeWire screen capture.
"obs/az-default/scenes.json".text = ''
{
"sources": [
{
"name": "Bildschirm (PipeWire)",
"id": "pipewire-screen-capture-source",
"type": "input"
},
{
"name": "Mikrofon",
"id": "pulse_input_capture",
"type": "input"
},
{
"name": "System-Audio",
"id": "pulse_output_capture",
"type": "input"
}
],
"scene": "AZ-Default"
}
'';
};
};
};
}
-45
View File
@@ -1,45 +0,0 @@
# roles/thin-client/apps/office-web.nix
#
# Office 365 (cloud) web apps as Chromium app-mode .desktop files.
# Q11 decisions: Word, Excel, PowerPoint, Outlook, Teams via
# `chromium --app=<URL>`, system-wide, Outlook+Teams autostart at login.
#
# The .desktop files are installed system-wide via
# environment.systemPackages, so every user sees them in the KDE menu
# under "Office".
{config, lib, pkgs, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
# Helper: builds a Chromium app-mode .desktop file.
mkOfficeWebDesktop = name: url: comment:
pkgs.writeTextFile {
name = builtins.replaceStrings [" "] ["-"] (lib.toLower name) + ".desktop";
destination = "/share/applications/${builtins.replaceStrings [" "] ["-"] (lib.toLower name)}.desktop";
text = ''
[Desktop Entry]
Version=1.0
Type=Application
Name=${name}
GenericName=${name} (Web)
Comment=${comment}
Exec=${pkgs.chromium}/bin/chromium --app=${url} --no-default-browser-check --no-first-run
Icon=web-browser
Terminal=false
Categories=Office;Network;WebApps;
Keywords=office;${lib.toLower name};web;
StartupNotify=true
StartupWMClass=${url}
'';
};
in {
config = mkIf cfg.enable {
environment.systemPackages = [
(mkOfficeWebDesktop "Word" "https://www.office.com/launch/word" "Microsoft Word Online")
(mkOfficeWebDesktop "Excel" "https://www.office.com/launch/excel" "Microsoft Excel Online")
(mkOfficeWebDesktop "PowerPoint" "https://www.office.com/launch/powerpoint" "Microsoft PowerPoint Online")
(mkOfficeWebDesktop "Outlook" "https://outlook.office.com" "Microsoft Outlook Web")
(mkOfficeWebDesktop "Teams" "https://teams.microsoft.com" "Microsoft Teams Web")
];
};
}
-96
View File
@@ -1,96 +0,0 @@
# roles/thin-client/apps/remmina.nix
#
# Remmina RDP client with system-wide connection profiles.
# Q10 decisions: System-wide /etc/remmina/, Kerberos SSO, several TS,
# Fullscreen+Multi-Monitor, only Audio local redirection.
#
# Note on "Several TS": we ship a small placeholder list with one example.
# Add real TS endpoints in az.tc.rdp.servers below.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
# Helper: builds a .remmina profile for one TS
mkRemminaProfile = server: let
name = server.name;
fqdn = server.fqdn;
in
pkgs.writeTextFile {
name = "${lib.toLower name}.remmina";
destination = "/etc/remmina/${lib.toLower name}.remmina";
text = ''
[remmina]
name=${name}
protocol=RDP
server=${fqdn}
username=
domain=AZ-GROUP
security=
colordepth=32
resolution=multimonitor
viewmode=2
fullscreen=1
multimon=1
audio-mode=1
audiocapture=0
clipboard=both
quality=2
kerberos=1
enable-authecol=kerberos
disableclipboard=0
redirectsound=1
redirectprinter=0
redirectdrive=0
redirectusb=0
keyboard-grab=1
showcursor=1
'';
};
in {
options.az.tc.rdp = {
servers = mkOption {
type = types.listOf (types.submodule {
options = {
name = mkOption {
type = types.str;
description = "Display name (e.g. 'TS Berlin', 'TS ERP').";
};
fqdn = mkOption {
type = types.str;
description = "FQDN of the TS endpoint (e.g. 'ts-berlin.az-group.local').";
};
};
});
default = [
# TODO: fill real TS endpoints
{
name = "TS Berlin";
fqdn = "ts-berlin.az-group.local";
}
];
description = ''
List of Terminal Server endpoints. One Remmina profile per entry
is generated system-wide.
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
remmina
freerdp
];
# System-wide Remmina profiles
environment.etc = builtins.listToAttrs (builtins.map (server: {
name = "remmina/${lib.toLower server.name}.remmina";
value.source = (mkRemminaProfile server) + "/etc/remmina/${lib.toLower server.name}.remmina";
})
config.az.tc.rdp.servers);
# Allow Domain Users to read the system-wide profiles
systemd.tmpfiles.rules = [
"d /etc/remmina 0755 root root -"
];
};
}
-82
View File
@@ -1,82 +0,0 @@
# roles/thin-client/apps/rustdesk.nix
#
# RustDesk client (Hilfe annehmen) + daemon (unattended support).
# Q13 decisions: Beides (Client+Daemon), Self-hosted Server, Pre-Shared Key
# via agenix, Wayland portal pre-authorized, Permanent password via agenix.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
hostname = config.networking.hostName;
in {
options.az.tc.rustdesk = {
serverHost = mkOption {
type = types.str;
default = "rustdesk.az-group.local"; # TODO: real host
description = "Self-hosted RustDesk hbbs/hbbr endpoint (via NetBird).";
};
serverPort = mkOption {
type = types.int;
default = 21116;
};
};
config = mkIf cfg.enable {
environment = {
systemPackages = with pkgs; [rustdesk];
etc = {
# /etc/rustdesk/RustDesk.toml — daemon config. Pre-shared key and
# server endpoints go here. Permanent password is in a separate
# agenix secret and written to a path RustDesk reads.
"rustdesk/RustDesk.toml".text = ''
[options]
custom-rendezvous-server = ${config.az.tc.rustdesk.serverHost}
relay-server = ${config.az.tc.rustdesk.serverHost}
api-server = https://${config.az.tc.rustdesk.serverHost}
key = file:/run/agenix/rustdesk-psk
verification-method = fixed-password
permanent-password-file = /run/agenix/${hostname}-rustdesk-password
enable-wayland-screen-share = true
allow-auto-disconnect = false
'';
# Wayland portal pre-authorization for RustDesk — KDE-specific
# xdg portal config so the "screen capture" prompt is pre-approved.
"xdg-desktop-portal/kde-screen-share.conf".text = ''
[allowed]
rustdesk=true
'';
};
};
# Systemd service: RustDesk daemon (runs as root for unattended).
systemd.services.rustdesk = {
description = "RustDesk unattended support daemon";
wantedBy = ["multi-user.target"];
after = ["network-online.target" "age-identity.service"];
wants = ["network-online.target" "age-identity.service"];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.rustdesk}/bin/rustdesk --service";
Restart = "always";
RestartSec = "5s";
User = "root";
};
};
# Shared pre-shared key (one for all thin clients)
age.secrets."rustdesk-psk" = {
file = ../../../secrets/rustdesk-psk.age;
mode = "0400";
owner = "root";
};
# Per-host permanent password
age.secrets."${hostname}-rustdesk-password" = {
file = ../../../secrets/${hostname}-rustdesk-password.age;
mode = "0400";
owner = "root";
};
};
}
-134
View File
@@ -1,134 +0,0 @@
# roles/thin-client/default.nix
#
# Top-level NixOS role module for the AZ-NIX-CLIENTS Thin Client fleet
# (AZ-TC-01..NN). Replaces a Windows 10 workstation with a locked-down
# NixOS + KDE Plasma client that authenticates against Active Directory
# and provides pre-configured RDP, browser, and Office-Web access.
#
# A Fleet Host (hosts/AZ-TC-NN/default.nix) only needs to set:
# az.tc.enable = true;
# az.tc.hardwareClass = "dell-optiplex-micro";
# networking.hostName = "AZ-TC-01";
# Everything else is composed by this role.
#
# See roles/thin-client/README.md for the provisioning workflow.
{
config,
lib,
pkgs,
...
}: let
cfg = config.az.tc;
in {
imports = [
./hardware
./session
./identity
./network
./peripherals
./apps
./monitoring
./deployment
];
options.az.tc = {
enable = lib.mkEnableOption "the AzIntec Thin Client role (KDE Plasma + AD + RDP fleet host)";
hardwareClass = lib.mkOption {
type = lib.types.enum ["dell-optiplex-micro" "generic-x86_64-uefi"];
default = "generic-x86_64-uefi";
description = ''
Hardware class of the Thin Client. Selects the appropriate
kernel modules, firmware, and video driver under
`roles/thin-client/hardware/`.
'';
};
site = lib.mkOption {
type = lib.types.str;
default = "default";
description = ''
Site identifier (e.g. "BER", "MUC"). Currently informational;
reserved for per-site printer maps or branding variations.
'';
};
};
config = lib.mkIf cfg.enable {
# ── Fleet-wide base configuration ────────────────────────────────
# Everything that every Thin Client needs regardless of which
# submodule pulls it in. Submodules opt-in via mkIf themselves.
# We do NOT import hosts/common here — that pulls nushell as default
# shell and m3ta-home profile system, neither of which belongs on a
# Thin Client. The minimum system config is set explicitly below.
nixpkgs.hostPlatform = "x86_64-linux";
nixpkgs.config.allowUnfree = true;
nix = {
settings = {
experimental-features = "nix-command flakes";
trusted-users = ["root"];
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 14d";
};
optimise.automatic = true;
};
# Thin Clients are managed centrally; users do not run nix commands.
users.defaultUserShell = pkgs.bash;
# Console / i18n / time — matches AZ-LT-NIX conventions.
i18n.defaultLocale = "de_DE.UTF-8";
time.timeZone = "Europe/Berlin";
console.useXkbConfig = true;
# Persistence-relevant state lives on the BTRFS root; nothing
# tmpfs-related here. /home persists per-user (Q6: "alles persistieren").
# Polkit for udisks/colord/etc. — needed so non-root users can mount
# USB sticks, change brightness, etc.
security.polkit.enable = true;
# XDG portal wiring for Wayland screen-capture (RustDesk + OBS).
environment.pathsToLink = [
"/share/xdg-desktop-portal"
"/share/applications"
];
# SSH is enabled by the network module (via NetBird only).
# Firewall is enabled by the network module.
# ── assertions / warnings ────────────────────────────────────────
# Catches un-overridden placeholders at build time so the operator
# notices BEFORE deploying to a real Thin Client. Each assertion
# checks a "placeholder" marker value; override the option in
# hosts/AZ-TC-NN/default.nix to silence.
assertions = [
{
assertion = cfg.enable -> (config.networking.hostName or "") != "";
message = "az.tc.enable requires networking.hostName to be set (e.g. 'AZ-TC-01').";
}
{
assertion = cfg.enable -> config.az.tc.wifi.ssid != "AZ-CORP" || config.az.tc.site == "staging";
message = ''
az.tc.wifi.ssid is still the placeholder 'AZ-CORP'. Set the
real corp WiFi SSID in hosts/AZ-TC-NN/default.nix (or set
az.tc.site = "staging" to silence for lab/test hosts).
'';
}
{
assertion = cfg.enable -> config.az.tc.branding.hotline != "+49 30 1234567" || config.az.tc.site == "staging";
message = "az.tc.branding.hotline is still the placeholder — set the real IT-Hotline number.";
}
{
assertion = cfg.enable -> config.az.tc.branding.company != "AzIntec GmbH" || config.az.tc.site == "staging";
message = "az.tc.branding.company is still the placeholder 'AzIntec GmbH' — set the real company name.";
}
];
};
}
@@ -1,33 +0,0 @@
# roles/thin-client/deployment/auto-upgrade.nix
#
# Q18 decision: Auto-Upgrade pro Host daily, Generation-Picker + BTRFS-
# Snapshot für Rollback, Auto-Reboot nach Kernel-Update.
{config, lib, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
system.autoUpgrade = {
enable = true;
dates = "03:00";
randomizedDelaySec = "30min";
flake = "git+https://code.m3ta.dev/m3tam3re/AZ-NIX-CLIENTS";
flags = [
"--no-write-lock-file"
"--refresh"
];
allowReboot = true;
rebootWindow = {
lower = "03:00";
upper = "05:00";
};
};
# Always keep at least 7 generations for rollback
boot.loader.systemd-boot.configurationLimit = 10;
# gc — keep more generations than the autoUpgrade default to allow
# rollback over a weekend.
nix.gc.options = "--delete-older-than 14d";
};
}
-12
View File
@@ -1,12 +0,0 @@
# roles/thin-client/deployment/default.nix
#
# Deployment layer: Disko BTRFS layout, auto-upgrade, snapper snapshots.
# Q3 (nixos-anywhere), Q4 (BTRFS, @root/@home), Q18 (auto-upgrade daily,
# generation-picker + BTRFS snapshot, auto-reboot).
{config, lib, ...}: {
imports = [
./disko.nix
./auto-upgrade.nix
./snapper.nix
];
}
-94
View File
@@ -1,94 +0,0 @@
# roles/thin-client/deployment/disko.nix
#
# BTRFS disk layout via Disko. Used by `nixos-anywhere` during provisioning.
# Q4 decisions: Standard persistent BTRFS, subvolumes @root and @home,
# zstd compression, snapper for / (NOT /home — privacy for multi-user).
#
# Apply during provisioning:
# nixos-anywhere --flake .#AZ-TC-01 \
# --disko disko-config ./roles/thin-client/deployment/disko.nix \
# root@<target-ip>
#
# This module sets `disko.devices` so the config can be referenced both
# for partitioning (nixos-anywhere --disko) and for fstab generation.
{config, lib, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
in {
options.az.tc.deployment = {
diskDevice = mkOption {
type = types.str;
default = "/dev/sda";
description = ''
Target disk device for the BTRFS layout. Typically /dev/sda or
/dev/nvme0n1. Override per-host in hosts/AZ-TC-NN/default.nix.
'';
};
swapSizeGB = mkOption {
type = types.int;
default = 4;
description = "Swap partition size in GB. Set 0 to disable swap.";
};
};
config = mkIf cfg.enable {
disko.devices = {
disk.main = {
type = "disk";
device = config.az.tc.deployment.diskDevice;
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = ["umask=0077"];
};
};
swap = mkIf (config.az.tc.deployment.swapSizeGB > 0) {
size = "${toString config.az.tc.deployment.swapSizeGB}G";
content = {
type = "swap";
randomEncryption = true;
};
};
root = {
size = "100%";
content = {
type = "btrfs";
extraArgs = ["-f"]; # force overwrite
subvolumes = {
"@root" = {
mountpoint = "/";
mountOptions = ["compress=zstd" "noatime" "ssd"];
};
"@home" = {
mountpoint = "/home";
mountOptions = ["compress=zstd" "noatime" "ssd"];
};
"@nix" = {
mountpoint = "/nix";
mountOptions = ["compress=zstd" "noatime" "ssd"];
};
"@persist" = {
mountpoint = "/persist";
mountOptions = ["compress=zstd" "noatime" "ssd"];
};
"@snapshots" = {
mountpoint = "/.snapshots";
mountOptions = ["compress=zstd" "noatime" "ssd"];
};
};
};
};
};
};
};
};
};
}
-40
View File
@@ -1,40 +0,0 @@
# roles/thin-client/deployment/snapper.nix
#
# BTRFS snapshots for the root subvolume via snapper. Snapshots taken
# before and after each `nixos-rebuild switch`, plus hourly/daily.
#
# Q18 decision: Generation-Picker + BTRFS-Snapshot.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
services.snapper = {
snapshotRootOnBoot = true;
configs = {
root = {
SUBVOLUME = "/";
ALLOW_USERS = ["root"];
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
TIMELINE_LIMIT_HOURLY = "12";
TIMELINE_LIMIT_DAILY = "7";
TIMELINE_LIMIT_WEEKLY = "4";
TIMELINE_LIMIT_MONTHLY = "0";
};
};
};
# Take a snapshot before any nixos-rebuild switch — hooks into the
# toplevel system path activation.
system.activationScripts.snapperPreSwitch = {
deps = [];
text = ''
if [ -d /.snapshots ]; then
${pkgs.snapper}/bin/snapper -c root create \
-d "pre-switch $(date +%Y-%m-%d-%H%M)" || true
fi
'';
};
};
}
-29
View File
@@ -1,29 +0,0 @@
# roles/thin-client/hardware/default.nix
#
# Dispatcher that pulls in the hardware module matching `az.tc.hardwareClass`.
# Each concrete module (e.g. dell-optiplex-micro.nix) sets kernel modules,
# video drivers, firmware, and kernel parameters appropriate for that SKU.
{
config,
lib,
...
}: let
cfg = config.az.tc;
in {
imports = [
./dell-optiplex-micro.nix
./generic-x86_64-uefi.nix
];
# The actual dispatch happens via mkIf inside each module, checking
# `config.az.tc.hardwareClass == "<this class>"`.
# Adding assertions here so misconfiguration is loud.
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = builtins.elem cfg.hardwareClass ["dell-optiplex-micro" "generic-x86_64-uefi"];
message = "az.tc.hardwareClass must be one of [dell-optiplex-micro generic-x86_64-uefi], got: ${cfg.hardwareClass}";
}
];
};
}
@@ -1,51 +0,0 @@
# roles/thin-client/hardware/dell-optiplex-micro.nix
#
# Hardware class for Dell OptiPlex Micro (3020/3040/5040/7040).
# These are 5th–7th gen Intel Core mini-PCs, all using Intel i915 graphics,
# all UEFI-bootable. Driver-wise they're nearly identical.
#
# Notes:
# - 3020 (Broadwell, 5th gen) — slightly older firmware, but i915 supports it.
# - 3040/5040 (Skylake, 6th gen) — mainstream.
# - 7040 (Skylake/Kaby Lake, 6th/7th gen) — some vPro variants exist.
# All variants: Intel ME present (mostly inactive unless explicitly provisioned).
{
config,
lib,
pkgs,
...
}: {
config = lib.mkIf (config.az.tc.enable && config.az.tc.hardwareClass == "dell-optiplex-micro") {
boot = {
loader.systemd-boot.enable = true;
loader.efi.canTouchEfiVariables = true;
initrd.kernelModules = ["i915" "snd_hda_intel"];
kernelModules = ["i915" "thinkpad_acpi" "coretemp"];
kernelParams = [
# Quiet boot for kiosk-like feel
"quiet"
"splash"
# Avoid rare i915 glitches on Skylake
"i915.enable_guc=2"
];
kernelPackages = pkgs.linuxPackages_latest;
};
services.xserver.videoDrivers = ["modesetting"];
# Firmware for Intel WiFi/BT on these models
hardware.enableRedistributableFirmware = true;
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
# Audio via Pipewire (set up in session/default.nix); just ensure
# sound firmware is present.
hardware.firmware = lib.mkDefault [pkgs.sof-firmware];
# Suspend/wake — Thin Clients are typically always-on; disable sleep
# to avoid Wake-on-LAN issues on the Dell NIC.
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
systemd.targets.hibernate.enable = false;
systemd.targets.hybrid-sleep.enable = false;
};
}
@@ -1,26 +0,0 @@
# roles/thin-client/hardware/generic-x86_64-uefi.nix
#
# Fallback hardware class for any UEFI-bootable x86_64 mini-PC that isn't
# explicitly listed. Used during pilot if a new SKU shows up, prevents
# deploy from blocking.
{config, lib, pkgs, ...}: {
config = lib.mkIf (config.az.tc.enable && config.az.tc.hardwareClass == "generic-x86_64-uefi") {
boot = {
loader.systemd-boot.enable = true;
loader.efi.canTouchEfiVariables = true;
initrd.kernelModules = ["i915" "amdgpu" "snd_hda_intel"];
kernelModules = ["kvm-intel" "kvm-amd" "coretemp"];
kernelParams = ["quiet"];
kernelPackages = pkgs.linuxPackages_latest;
};
services.xserver.videoDrivers = ["modesetting" "amdgpu"];
hardware.enableRedistributableFirmware = true;
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
systemd.targets.hibernate.enable = false;
systemd.targets.hybrid-sleep.enable = false;
};
}
-171
View File
@@ -1,171 +0,0 @@
# roles/thin-client/identity/ad.nix
#
# Active Directory integration for Thin Clients.
#
# Real realm: AZ-GROUP
# DNS domain: az-group.local
# Join mechanism: Pre-created computer accounts + keytab via agenix
# (no interactive realm join, no service account with join privileges).
#
# Provisioning per host (admin-side, one-shot):
# 1. adcli precreate --computer-name=AZ-TC-01$ \
# --domain=az-group.local \
# --host-fqdn=AZ-TC-01.az-group.local \
# --login-type=computer \
# --os-name="NixOS" --os-version="25.11" \
# --domain-ou="OU=ThinClients,DC=az-group,DC=local" \
# <admin>@AZ-GROUP
# 2. adcli join --computer-name=AZ-TC-01$ \
# --domain=az-group.local \
# --host-fqdn=AZ-TC-01.az-group.local \
# --login-type=computer \
# --user=<admin> --verbose \
# -K /tmp/AZ-TC-01.keytab
# 3. agenix -e secrets/AZ-TC-01-krb5-keytab.age (paste /tmp/AZ-TC-01.keytab)
# 4. rm /tmp/AZ-TC-01.keytab
# 5. Deploy; the keytab decrypts to /etc/krb5.keytab on the host.
#
# SSSD resolves users/groups via AD; kerberos auth via keytab for the
# machine account. User login uses their AD password (offline-capable via
# cache_credentials=true).
{
config,
lib,
pkgs,
...
}: let
cfg = config.az.tc;
hostname = config.networking.hostName;
domain = "az-group.local";
realm = "AZ-GROUP";
in {
options.az.tc.ad = {
ou = lib.mkOption {
type = lib.types.str;
default = "OU=ThinClients,DC=az-group,DC=local";
description = ''
AD Organizational Unit where Thin Client computer objects live.
Override if your AD layout differs. Used for documentation and
the pre-create workflow; not consumed at runtime.
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [
adcli # AD computer-account precreate + join helper
sssd # AD client daemon
krb5 # MIT Kerberos client
realmd # DBus service for realm discovery (used by adcli wrapper)
# NOTE: samba4Full intentionally NOT included — it would pull in
# ceph-common which is currently broken in nixpkgs-unstable
# (python metadata issue). Thin Clients don't need Samba server
# or smbclient — share mounting uses cifs-utils (in smb-mounts.nix).
# If `net` or `smbclient` are ever needed, install on the admin
# workstation (AZ-LT-NIX), not on the Thin Client.
];
# Kerberos client
security.krb5 = {
enable = true;
settings = {
libdefaults = {
default_realm = realm;
udp_preference_limit = 0;
forwardable = true;
proxiable = true;
rdns = false;
};
domain_realm = {
".az-group.local" = realm;
"az-group.local" = realm;
};
realms = {
"${realm}" = {
kdc = ["az-dc01.az-group.local" "az-dc02.az-group.local"];
admin_server = "az-dc01.az-group.local";
};
};
};
};
# PAM: create per-user /home on first login (used by domain users)
security.pam = {
services.login.makeHomeDir = true;
services.sddm.makeHomeDir = true;
services.sshd.makeHomeDir = true;
makeHomeDir.umask = "077";
};
services.nscd.enable = true;
services.sssd = {
enable = true;
config = ''
[sssd]
domains = az-group.local
config_file_version = 2
services = nss, pam
[domain/az-group.local]
id_provider = ad
auth_provider = ad
access_provider = ad
chpass_provider = ad
ad_domain = ${domain}
ad_server = az-dc01.az-group.local, az-dc02.az-group.local
krb5_realm = ${realm}
krb5_server = az-dc01.az-group.local, az-dc02.az-group.local
krb5_keytab = /etc/krb5.keytab
krb5_store_password_if_offline = True
cache_credentials = True
use_fully_qualified_names = false
fallback_homedir = /home/%u
override_shell = /run/current-system/sw/bin/bash
default_shell = /run/current-system/sw/bin/bash
ad_gpo_access_control = permissive
enumerate = true
ldap_id_mapping = false
'';
};
# DNS — Thin Clients use AD DCs as resolver (NetBird DNS overlays
# for corp-VPN-only domains via systemd-resolved; see network/dns.nix)
networking.nameservers = lib.mkDefault ["az-dc01.az-group.local" "az-dc02.az-group.local"];
networking.domain = lib.mkDefault domain;
networking.search = lib.mkDefault [domain];
# Host FQDN — important for Kerberos SPN matching.
# `networking.hostName` is set by the host's default.nix; here we
# only add the hosts file fallback so the FQDN resolves locally even
# before DNS is reachable.
networking.extraHosts = ''
127.0.0.1 ${hostname}.${domain} ${hostname}
'';
# agenix-deployed machine keytab
age.secrets."${hostname}-krb5-keytab" = {
file = ../../../secrets/${hostname}-krb5-keytab.age;
path = "/etc/krb5.keytab";
mode = "0600";
owner = "root";
group = "root";
};
# Ensure keytab path is readable by sssd (runs as root) but not by
# anyone else.
systemd.services.sssd = {
after = ["age-identity.service"];
wants = ["age-identity.service"];
serviceConfig.ExecStartPre = let
check = pkgs.writeShellScript "check-keytab" ''
if [ ! -s /etc/krb5.keytab ]; then
echo "ERROR: /etc/krb5.keytab is empty or missing." >&2
echo "Provision via adcli join + agenix, see roles/thin-client/README.md." >&2
exit 1
fi
'';
in ["+${check}"];
};
};
}
-16
View File
@@ -1,16 +0,0 @@
# roles/thin-client/identity/default.nix
#
# Identity layer: Active Directory integration (realm/sssd/krb5 via keytab),
# local break-glass users (without m3ta-home), sudo policy.
#
# All AD-related secrets are provisioned via agenix:
# secrets/<hostname>-krb5-keytab.age → /etc/krb5.keytab
#
# See README.md for the pre-create workflow.
{config, lib, ...}: {
imports = [
./ad.nix
./local-users.nix
./sudo.nix
];
}
@@ -1,41 +0,0 @@
# roles/thin-client/identity/local-users.nix
#
# Local break-glass users for Thin Clients. These exist independently of
# AD/DNS/NetBird availability. Used for disaster recovery via NetBird SSH.
#
# NOTE: This intentionally does NOT import the m3ta-home profile system
# from hosts/common/users/. Thin Clients are not dev machines and don't
# need home-manager profiles — just a lean account with password, SSH key,
# and wheel membership.
{
config,
lib,
...
}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
users.users."sascha.koenig" = {
# Re-uses the existing hashedPassword from hosts/common/users/sascha.koenig.nix
hashedPassword = "$y$j9T$ORX4btVZgs9Xjq2oIvzJm0$lXiPwaa0D6t.eMDIx1UBesEAMOkWXBoGwpeI7X0aS8D";
isNormalUser = true;
shell = config.users.defaultUserShell;
extraGroups = ["wheel" "networkmanager" "plugdev" "input"];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZbg/Z9mnflXuLahGY8WOSBMqbgeqVIkIwRkquys1Ml sascha.koenig@azintec.com"
];
};
users.users."jannik.mueller" = {
# Re-uses the existing hashedPassword from hosts/common/users/jannik.mueller.nix
hashedPassword = "$y$j9T$09RgD3AU3PK9Oi6JGLe0V1$i8J2ZOD1h1b6Zpw28ub.kExujoDKHzokeXzkM23Tfd/";
isNormalUser = true;
shell = config.users.defaultUserShell;
extraGroups = ["wheel" "networkmanager" "plugdev" "input"];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPvZazSuIoWoRWhkAqQDMLeurxVUyy1MTllp1wfw1tzq"
];
};
};
}
-34
View File
@@ -1,34 +0,0 @@
# roles/thin-client/identity/sudo.nix
#
# Sudo policy:
# - Members of "domain admins" may sudo WITH their password.
# - Local users in wheel (sascha.koenig, jannik.mueller) may sudo WITH
# their password.
# - Normal domain users may NOT sudo (everything they need — USB mount,
# audio, screen-lock — runs via polkit/udisks).
#
# Q7 decision: "Ja, aber mit Passwort" — passwordless sudo is disabled.
{
config,
lib,
...
}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
# Use extraConfig because sudoers syntax requires escaping the space
# in "domain admins" as `\ `. extraRules would not let us inject that.
security.sudo = {
enable = true;
execWheelOnly = true;
extraConfig = ''
# Domain Admins — sudo WITH password (Q7 decision).
# The backslash-space escapes the space in "domain admins".
%domain\ admins ALL=(ALL:ALL) PASSWD: ALL
Defaults:%domain\ admins env_keep+=TERMINFO_DIRS
Defaults:%domain\ admins env_keep+=TERMINFO
'';
};
};
}
-220
View File
@@ -1,220 +0,0 @@
# roles/thin-client/monitoring/default.nix
#
# Q19 decisions: Prometheus node_exporter + Loki promtail (now: Alloy) +
# NetBird Inventory + Asset-Management-Tool (default: Snipe-IT).
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
in {
options.az.tc.monitoring = {
prometheusPushGateway = mkOption {
type = types.str;
default = "pushgateway.az-group.local:9091"; # TODO: real
description = ''
Prometheus Pushgateway URL. node_exporter runs locally and pushes
metrics here (thin clients are usually behind NetBird NAT, so
pull-scraping isn't reliable).
'';
};
lokiUrl = mkOption {
type = types.str;
default = "http://loki.az-group.local:3100"; # TODO: real
description = "Loki URL for Alloy log shipping.";
};
assetTool = mkOption {
type = types.enum ["snipe-it" "it-glue" "glpi" "none"];
default = "snipe-it";
description = ''
Asset management tool to integrate with. snipe-it is the
open-source default. Override if you use a different one.
'';
};
snipeItUrl = mkOption {
type = types.str;
default = "https://snipeit.az-group.local"; # TODO: real
description = "Snipe-IT base URL (no trailing slash).";
};
# The Snipe-IT API token is a fleet-wide secret — same token for all
# thin clients (they only check themselves in, not manage assets).
# Stored in agenix under secrets/snipeit-api-token.age.
};
config = mkIf cfg.enable {
services.prometheus.exporters.node = {
enable = true;
enabledCollectors = ["systemd" "processes" "tcpstat"];
listenAddress = "127.0.0.1";
port = 9100;
};
# Push metrics to the central Pushgateway every 60s.
# Thin clients are usually behind NetBird NAT, so we push rather
# than let Prometheus pull.
systemd.services."push-node-metrics" = {
description = "Push node_exporter metrics to Pushgateway";
wantedBy = ["multi-user.target"];
after = ["network-online.target" "prometheus-node-exporter.service"];
wants = ["network-online.target"];
serviceConfig = {
Type = "simple";
ExecStart = pkgs.writeShellScript "push-node-metrics" ''
set -euo pipefail
while true; do
# Scrape local node_exporter and forward to Pushgateway.
# Use --data-binary to preserve Prometheus exposition format.
${pkgs.curl}/bin/curl -s --fail \
--connect-timeout 5 \
"http://127.0.0.1:9100/metrics" \
| ${pkgs.curl}/bin/curl -s --fail \
--connect-timeout 5 \
-X POST \
--data-binary @- \
"http://${config.az.tc.monitoring.prometheusPushGateway}/metrics/job/${config.networking.hostName}/instance/${config.networking.hostName}" \
|| echo "push-node-metrics: push failed, will retry in 60s" >&2
sleep 60
done
'';
Restart = "always";
RestartSec = "10s";
User = "root";
};
};
# Log shipping via Grafana Alloy (promtail is deprecated/EOL since
# 2025). Alloy scrapes the systemd journal and ships to Loki.
# Config is a real River config that ships all journal logs with
# host + unit labels.
services.alloy = {
enable = true;
extraFlags = [
"--server.http.listen.address=127.0.0.1"
"--server.http.listen.port=12345"
];
};
environment.etc."alloy/config.alloy".text = ''
// Auto-generated by roles/thin-client/monitoring/default.nix
// Ships all systemd journal entries to the central Loki.
logging {
level = "info"
format = "logfmt"
}
loki.write "default" {
endpoint {
url = "${config.az.tc.monitoring.lokiUrl}/loki/api/v1/push"
}
}
loki.source.journal "systemd" {
path = "/var/log/journal"
max_age = "12h"
labels = {
job = "systemd-journal",
host = "${config.networking.hostName}",
}
forward_to = [loki.write.default.receiver]
relabel_rules = {
rule {
source_labels = ["__journal__systemd_unit"]
target_label = "unit"
}
rule {
source_labels = ["__journal__priority"]
target_label = "severity"
}
rule {
source_labels = ["__journal__transport"]
target_label = "transport"
}
}
}
'';
# Snipe-IT asset check-in — reports host presence + serial + asset tag
# to the central asset management. Runs once at boot and daily after.
# Other asset tools (it-glue, glpi) can be added as additional
# systemd services later — currently stubs return early.
systemd.services.snipe-it-checkin = mkIf (config.az.tc.monitoring.assetTool == "snipe-it") {
description = "Report host presence to Snipe-IT asset management";
wantedBy = ["multi-user.target"];
after = ["network-online.target"];
wants = ["network-online.target"];
startAt = "*-*-* 03:30:00"; # daily at 03:30 (after auto-upgrade window)
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "snipe-it-checkin" ''
set -euo pipefail
API_URL="${config.az.tc.monitoring.snipeItUrl}/api/v1"
TOKEN_FILE="/run/agenix/snipeit-api-token"
if [ ! -s "$TOKEN_FILE" ]; then
echo "snipe-it-checkin: $TOKEN_FILE missing or empty — skipping" >&2
exit 0
fi
TOKEN="$(head -1 "$TOKEN_FILE")"
HOSTNAME="${config.networking.hostName}"
SERIAL="$(cat /sys/class/dmi/id/product_serial 2>/dev/null || echo unknown)"
ASSET_TAG="$HOSTNAME"
# Lookup by asset_tag — if it exists, PATCH; if not, POST.
EXISTING="$(${pkgs.curl}/bin/curl -s --fail \
--connect-timeout 5 \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"$API_URL/hardware/byasset/$ASSET_TAG" || echo '{}')"
PAYLOAD="$(cat <<JSON
{
"asset_tag": "$ASSET_TAG",
"name": "$HOSTNAME",
"serial": "$SERIAL",
"model_id": 1,
"status_id": 2,
"notes": "AZ-NIX-CLIENTS thin client (auto-checked-in)"
}
JSON
)"
if echo "$EXISTING" | ${pkgs.jq}/bin/jq -e '.id' >/dev/null 2>&1; then
ID="$(echo "$EXISTING" | ${pkgs.jq}/bin/jq -r '.id')"
${pkgs.curl}/bin/curl -s --fail \
--connect-timeout 5 \
-X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "$PAYLOAD" \
"$API_URL/hardware/$ID" >/dev/null
echo "snipe-it-checkin: PATCHed asset $ID ($ASSET_TAG)"
else
${pkgs.curl}/bin/curl -s --fail \
--connect-timeout 5 \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "$PAYLOAD" \
"$API_URL/hardware" >/dev/null
echo "snipe-it-checkin: POSTed new asset $ASSET_TAG"
fi
'';
User = "root";
};
};
# Snipe-IT API token (fleet-wide shared secret)
age.secrets."snipeit-api-token" = mkIf (config.az.tc.monitoring.assetTool == "snipe-it") {
file = ../../../secrets/snipeit-api-token.age;
mode = "0400";
owner = "root";
};
};
}
@@ -1,34 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDpTCCAo2gAwIBAgIUXpLpz2HAOH6Ts25kDS2ESUDavy0wDQYJKoZIhvcNAQEL
BQAwYjE8MDoGA1UEAwwzQVotTklYLUNMSUVOVFMgcGxhY2Vob2xkZXIgQ0EgUkVQ
TEFDRSBCRUZPUkUgREVQTE9ZMRUwEwYDVQQKDAxBekludGVjIEdtYkgxCzAJBgNV
BAsMAklUMB4XDTI2MDcyOTA2NDIxN1oXDTM2MDcyNjA2NDIxN1owYjE8MDoGA1UE
AwwzQVotTklYLUNMSUVOVFMgcGxhY2Vob2xkZXIgQ0EgUkVQTEFDRSBCRUZPUkUg
REVQTE9ZMRUwEwYDVQQKDAxBekludGVjIEdtYkgxCzAJBgNVBAsMAklUMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAubFUzQHuTkl6QbLowpwmhnTOxV0O
BCqlgxk3Ot5wSAE7RTJ2iHw3rGoSypZge4XekLLKuIw9YipjhZMSn1twP6dBV656
syNFnEc6NEAnvwm+j7XtRwLSeeLywEIEVrelVxwMwzqxSFJzt/N12krQLG/WY85c
nSjy5FVoGOybxHZyrW2eilkp/0zXlOylG0GHUNIDcTzyy83pAHQCEE4L+pIVAHOO
L3rtOrwIk/5foS+h423Gb+ik/TiqyzMBq9uWq//AeTutbniK0z5kTq2l1sNcDoYB
i8AijDehBcTv4xsHnki+xeEBikUB7tjzsJ3/J6aKFaoPC/roT4B3rOfT9QIDAQAB
o1MwUTAdBgNVHQ4EFgQUfjQZj2ntz7S9BmfwdIz30EBJOEYwHwYDVR0jBBgwFoAU
fjQZj2ntz7S9BmfwdIz30EBJOEYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
AQsFAAOCAQEAI8Yy9RBYnOiTcttfrJAQtmr7vExkmmPgdPwbVwjlLTysrbzcWeb8
cT3GDn+zyvBjnKNQAq0D8vp0l2oF9+BytTXKf/Ff2rfzgYOp4rnA3j6e7AMKxUZL
5ZEHA4iEORxxS8M/uxJN8xdjoirGb0rn0jdj3V71fxQOJ82WL0r1jfrcxyk8EPDi
Cbe8q5s0IJsaVLgFGGNEu/RwPIFkg4tZ5+PZeWX5ZGH9roQ78VzSsNO+AHIO0QO0
DIompYk1V6JiswlBDk+0MqQ8gt/jZFtW7Dc8+5k5zarhCEmlkk9QBzGA6Rse33bQ
1yLgSNm/KqNDysGZGFFB7Gih8IA/lrjivQ==
-----END CERTIFICATE-----
# ─────────────────────────────────────────────────────────────────
# This is a PLACEHOLDER certificate generated by the build setup.
# Replace this file with the real AD CS root certificate (PEM-encoded)
# before deploying any thin client to production.
#
# To obtain: export the "Root CA" certificate from AD CS (certlm.msc →
# Trusted Root Certification Authorities → Certificates → right-click →
# All Tasks → Export → Base-64 encoded X.509 (.CER)).
# This file is NOT secret — it's a public trust anchor. Committing it
# to the repo is fine.
# ─────────────────────────────────────────────────────────────────
-14
View File
@@ -1,14 +0,0 @@
# roles/thin-client/network/default.nix
#
# Network layer: NetworkManager + wpa_supplicant (NOT iwd, because iwd
# has weaker 802.1X support), 802.1X EAP-TLS WiFi, NetBird overlay,
# systemd-resolved for DNS splitting, hardened firewall.
{config, lib, ...}: {
imports = [
./wifi.nix
./netbird.nix
./dns.nix
./firewall.nix
./ssh.nix
];
}
-24
View File
@@ -1,24 +0,0 @@
# roles/thin-client/network/dns.nix
#
# DNS strategy: Corp DNS (AD DCs) as primary, NetBird DNS as overlay
# for NetBird-managed domains via systemd-resolved.
#
# Q15 decision: "Corp DNS + NetBird DNS parallel".
{config, lib, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
services.resolved = {
enable = true;
settings.Resolve = {
Domains = ["az-group.local"];
FallbackDNS = ["9.9.9.9" "1.1.1.1"];
LLMNR = "no";
MulticastDNS = "no";
};
};
# NetBird hooks into resolved via its own daemon (no extra config here).
};
}
-23
View File
@@ -1,23 +0,0 @@
# roles/thin-client/network/firewall.nix
#
# Hardened firewall: deny all inbound except NetBird interface and ICMP.
{config, lib, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
networking.firewall = {
enable = true;
allowPing = true;
# No inbound TCP/UDP ports opened — all access via NetBird.
allowedTCPPorts = [];
allowedUDPPorts = [];
# NetBird's WireGuard port (allowed by NetBird service itself,
# but make explicit here).
interfaces."wt0".allowedTCPPorts = []; # NetBird userspace daemon
trustedInterfaces = ["lo"];
checkReversePath = "loose";
};
};
}
-70
View File
@@ -1,70 +0,0 @@
# roles/thin-client/network/netbird.nix
#
# NetBird client for Thin Clients. Per-host setup-key via agenix.
# Q15 decisions: Per-Host Setup-Key, Corp DNS + NetBird DNS parallel,
# Client-only + accept-routes.
#
# SSH via NetBird: enabled (Q7).
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
hostname = config.networking.hostName;
in {
options.az.tc.netbird = {
setupKeyFile = mkOption {
type = types.path;
default = ../../../secrets/${hostname}-netbird-setupkey.age;
readOnly = true;
description = ''
agenix-encrypted NetBird setup key for this host. Provisioned in
the NetBird UI before first boot. Decrypted to
/run/agenix/netbird-setupkey (referenced by the systemd service).
'';
};
managementUrl = mkOption {
type = types.str;
default = "https://netbird.az-group.local:443"; # TODO: real URL
description = "NetBird management URL (self-hosted).";
};
};
config = mkIf cfg.enable {
services.netbird.enable = true;
systemd.services.netbird = {
environment = {
NB_DISABLE_SSH_CONFIG = "false"; # we want SSH-via-NetBird
NB_MGMT_URL = config.az.tc.netbird.managementUrl;
NB_SETUP_KEY = "file:/run/agenix/${hostname}-netbird-setupkey";
};
path = with pkgs; [shadow util-linux];
after = ["age-identity.service" "network-online.target"];
wants = ["age-identity.service" "network-online.target"];
};
# Per-host setup key (agenix)
age.secrets."${hostname}-netbird-setupkey" = {
file = config.az.tc.netbird.setupKeyFile;
mode = "0400";
owner = "root";
group = "root";
};
# SSH config: allow SSH via NetBird hosts (the netbird binary acts as
# a ProxyCommand when the target is a NetBird peer).
programs.ssh.extraConfig = ''
Match exec "${pkgs.netbird}/bin/netbird ssh detect %h %p"
PreferredAuthentications publickey
PubkeyAuthentication yes
PasswordAuthentication no
ProxyCommand ${pkgs.netbird}/bin/netbird ssh proxy %h %p
StrictHostKeyChecking accept-new
LogLevel ERROR
'';
# Loose reverse-path filter — required for overlay networks.
networking.firewall.checkReversePath = "loose";
};
}
-28
View File
@@ -1,28 +0,0 @@
# roles/thin-client/network/ssh.nix
#
# SSH enabled for admin access via NetBird (Q7).
# Hardened: no root login, only SSH key auth.
{config, lib, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PubkeyAuthentication = true;
};
# Only listen on the NetBird interface (wt0) — not on the LAN.
listenAddresses = [
{addr = "0.0.0.0"; port = 22;}
];
};
# Persistent SSH host keys — these survive reboot and are the identity
# NetBird and admin SSH uses. With BTRFS root + persistent /, this is
# automatic. (TODO for v2: move to /persist if we introduce impermanence.)
};
}
-117
View File
@@ -1,117 +0,0 @@
# roles/thin-client/network/wifi.nix
#
# 802.1X EAP-TLS WiFi via NetworkManager + wpa_supplicant backend.
# Q8 decisions: EAP-TLS, per-machine client cert, AD CS 3-5y lifetime,
# single Corp SSID.
#
# Per-host secrets (via agenix):
# secrets/<hostname>-wifi-client.pem.age
# → decrypted to /etc/wifi/client.pem
# Contains the full PKCS#12 export OR combined PEM (cert + key).
# Mode 0600, owner root.
#
# CA cert is NOT secret — checked into the repo as a public file under
# roles/thin-client/network/assets/corp-wifi-ca.pem. Replace placeholder
# with real AD CS root cert.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
hostname = config.networking.hostName;
in {
options.az.tc.wifi = {
ssid = mkOption {
type = types.str;
default = "AZ-CORP"; # TODO: real SSID
description = "Corporate SSID for thin clients.";
};
caCert = mkOption {
type = types.path;
default = ./assets/corp-wifi-ca.pem;
description = ''
Corporate WiFi CA certificate (PEM format, not secret — checked
into the repo). Replace the placeholder with the real AD CS root
cert.
'';
};
};
config = mkIf cfg.enable {
# Disable iwd explicitly — wpa_supplicant is the backend we use.
# Note: NetworkManager with `wifi.backend = "wpa_supplicant"` automatically
# sets networking.wireless.enable = true (it owns wpa_supplicant).
networking.wireless.iwd.enable = false;
networking.networkmanager = {
enable = true;
wifi.backend = "wpa_supplicant";
};
# Declarative NetworkManager profile for corp WiFi (EAP-TLS).
# NixOS has no built-in `ensureProfiles` option in the
# `networking.networkmanager` namespace, so we write the
# `.nmconnection` file directly to the system-connections dir.
# NetworkManager picks it up on restart.
environment.etc."NetworkManager/system-connections/${config.az.tc.wifi.ssid}.nmconnection".source =
pkgs.writeText "${config.az.tc.wifi.ssid}.nmconnection" ''
[connection]
id=${config.az.tc.wifi.ssid}
type=wifi
autoconnect=true
permissions=
[wifi]
mode=infrastructure
ssid=${config.az.tc.wifi.ssid}
[wifi-security]
key-mgmt=wpa-eap
[802-1x]
eap=tls
identity=${hostname}$
ca-cert=${config.az.tc.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
'';
# `environment.etc` files are owned by root but world-readable by
# default — make this profile root-only since it references the cert
# path (the cert itself is root-only via agenix).
systemd.tmpfiles.rules = [
"d /etc/wifi 0700 root root -"
];
# NOTE: We intentionally do NOT add the WiFi CA to
# security.pki.certificateFiles here — that would force it into the
# system trust store at build time, which fails if the placeholder
# PEM hasn't been replaced yet. NetworkManager references the CA
# directly via the `ca-cert=` key in the .nmconnection profile, which
# is sufficient for 802.1X EAP-TLS. Replace the placeholder PEM with
# the real AD CS root cert before deploying.
# Per-host client cert (via agenix)
age.secrets."${hostname}-wifi-client-cert" = {
file = ../../../secrets/${hostname}-wifi-client-cert.age;
path = "/etc/wifi/client.pem";
mode = "0600";
owner = "root";
group = "root";
};
# Directory for the cert — NetworkManager reads it as root.
# Ensure wpa_supplicant doesn't try to use the cert before agenix decrypted it.
systemd.services.NetworkManager-wait-online = {
after = ["age-identity.service"];
wants = ["age-identity.service"];
};
};
}
@@ -1,7 +0,0 @@
# roles/thin-client/peripherals/default.nix
{config, lib, ...}: {
imports = [
./printing.nix
./smb-mounts.nix
];
}
@@ -1,54 +0,0 @@
# roles/thin-client/peripherals/printing.nix
#
# CUPS with the single Pull-Print queue. Q16 decision: All users print
# to one queue, retrieve at any physical device via badge/PIN.
#
# Driverless IPPS — no vendor driver needed.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
in {
options.az.tc.printing = {
pullPrintEndpoint = mkOption {
type = types.str;
default = "ipps://pull-print.az-group.local:443/ipp/print"; # TODO: real endpoint
description = ''
IPPS URI of the Pull-Print queue. All thin clients print here.
'';
};
queueName = mkOption {
type = types.str;
default = "Pull-Print";
description = "Name of the local CUPS queue for the Pull-Print endpoint.";
};
};
config = mkIf cfg.enable {
services.printing = {
enable = true;
drivers = with pkgs; [cups-filters];
browsing = false;
listenAddresses = ["localhost:631"];
allowFrom = ["localhost"];
};
# No Avahi — Q16 decision: "Avahi aus".
services.avahi.enable = false;
# Single Pull-Print queue
hardware.printers = {
ensurePrinters = [
{
name = config.az.tc.printing.queueName;
location = "Pull-Print";
description = "Central Pull-Print queue (follow-me print)";
deviceUri = config.az.tc.printing.pullPrintEndpoint;
model = "everywhere";
ppdOptions = {PageSize = "A4";};
}
];
ensureDefaultPrinter = config.az.tc.printing.queueName;
};
};
}
@@ -1,106 +0,0 @@
# roles/thin-client/peripherals/smb-mounts.nix
#
# pam_mount: Windows shares auto-mount at login via Kerberos.
# Q9 decisions: pam_mount, Per-User + Common Shares via DFS-Namespace,
# Lax (mount failure doesn't block login).
#
# Kerberos semantics:
# sec=krb5i integrity-protected Kerberos auth
# multiuser each user gets own mount credentials (no shared session)
# cruid=%(USER) the credential user (pam_mount templates it)
# nodepray don't attempt password-based retry
#
# Mounts under /mnt/az-dfs/<share>; KDE desktop shows symlinks.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf mkOption types;
cfg = config.az.tc;
in {
options.az.tc.smb = {
dfsNamespace = mkOption {
type = types.str;
default = "\\\\az-group.local\\dfs"; # TODO: confirm
description = ''
DFS namespace root that all share paths are relative to.
Example: \\az-group.local\dfs
'';
};
userShare = mkOption {
type = types.str;
default = "users/%u";
description = ''
Per-user share path relative to the DFS namespace. %u is
expanded to the username by pam_mount.
'';
};
commonShares = mkOption {
type = types.listOf types.str;
default = ["public" "software"];
description = ''
Common shares (relative to DFS namespace) that every user gets
mounted at login.
'';
};
mountRoot = mkOption {
type = types.str;
default = "/mnt/az-dfs";
description = ''
Local mount root. Shares appear under this path as
<mountRoot>/<share-name>. User's personal share appears as
<mountRoot>/home.
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
cifs-utils
pam_mount
];
# pam_mount config — extraVolumes is a `list of string`, each one
# a complete <volume>...</volume> XML element appended to
# /etc/pam_mount.conf.xml by the NixOS pam_mount module.
security.pam.mount = let
dfs = config.az.tc.smb.dfsNamespace;
mnt = config.az.tc.smb.mountRoot;
# Per-user share
userVolume = ''
<volume
fstype="cifs"
server="${dfs}"
path="${config.az.tc.smb.userShare}"
mountpoint="${mnt}/home"
options="sec=krb5i,cruid=%(USER),uid=%(USER),gid=%(USER),multiuser,nodev,nosuid,mfsymlinks"
user="*"
/>
'';
# Common shares (read-only)
commonVolumes = builtins.map (share: ''
<volume
fstype="cifs"
server="${dfs}"
path="${share}"
mountpoint="${mnt}/${share}"
options="sec=krb5i,cruid=%(USER),uid=%(USER),gid=%(USER),multiuser,nodev,nosuid,ro,mfsymlinks"
user="*"
/>
'') config.az.tc.smb.commonShares;
in {
enable = true;
extraVolumes = [userVolume] ++ commonVolumes;
};
# Ensure mount directories exist (created per-user at first login).
systemd.tmpfiles.rules = [
"d ${config.az.tc.smb.mountRoot} 0755 root root -"
];
# Create /etc/krb5.keytab is owned by root and readable to pam_mount
# (which runs as root during PAM).
# (No additional setup needed — pam_mount reads the user's TGT, not
# the machine keytab.)
};
}
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Placeholder AzIntec logo. Replace with real corporate logo. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 80" width="200" height="80">
<rect width="200" height="80" fill="#1e3a8a"/>
<text x="100" y="50" font-family="Arial,sans-serif" font-size="24" font-weight="bold" fill="white" text-anchor="middle">AZ-INTEC</text>
</svg>

Before

Width:  |  Height:  |  Size: 388 B

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Placeholder wallpaper. Replace with real corporate wallpaper (JPEG preferred). -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" width="1920" height="1080">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0f172a"/>
<stop offset="100%" stop-color="#1e3a8a"/>
</linearGradient>
</defs>
<rect width="1920" height="1080" fill="url(#bg)"/>
<text x="960" y="540" font-family="Arial,sans-serif" font-size="72" font-weight="bold" fill="white" text-anchor="middle" opacity="0.3">AZ-INTEC</text>
</svg>

Before

Width:  |  Height:  |  Size: 626 B

-25
View File
@@ -1,25 +0,0 @@
# roles/thin-client/session/audio.nix
#
# PipeWire audio stack. Required by RustDesk (capture/playback), OBS,
# Chromium (Office Web, Teams Web). Done here rather than in apps/ because
# it's session infrastructure.
{config, lib, pkgs, ...}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = true;
wireplumber.enable = true;
};
environment.systemPackages = with pkgs; [
pulseaudioFull # pactl/pavucontrol for support cases
pavucontrol
];
};
}
-86
View File
@@ -1,86 +0,0 @@
# roles/thin-client/session/branding.nix
#
# Light branding: corporate wallpaper, SDDM logo overlay, Breeze Light,
# Property-of-footer (hostname + IT hotline) on the lock screen.
# Q17 decisions: Light Branding, no login banner, user may adjust KDE.
{
config,
lib,
pkgs,
...
}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
options.az.tc.branding = {
hotline = lib.mkOption {
type = lib.types.str;
default = "+49 30 1234567"; # TODO: real hotline
description = "IT-Hotline phone number for the property-of footer.";
};
wallpaper = lib.mkOption {
type = lib.types.path;
default = ./assets/wallpaper.svg;
description = ''
Path to a corporate wallpaper image (JPEG or SVG). Replace
`roles/thin-client/session/assets/wallpaper.svg` with the real
asset, or override this option.
'';
};
logo = lib.mkOption {
type = lib.types.path;
default = ./assets/logo.svg;
description = ''
Path to the corporate logo (SVG or PNG). Used in SDDM and lock
screen footer.
'';
};
company = lib.mkOption {
type = lib.types.str;
default = "AzIntec GmbH";
description = "Company name shown in the property-of footer.";
};
};
config = mkIf cfg.enable {
environment = {
etc = {
# Wallpaper — install to /etc/az-wallpaper so KDE's wallpaper plugin
# can find it via the standard search path.
"az-wallpaper".source = config.az.tc.branding.wallpaper;
# Default Plasma configuration as /etc/xdg — KDE reads these as
# the system-wide defaults. User-specific changes are layered on top.
"xdg/kdeglobals".text = ''
[General]
ColorScheme=Breeze Light
[KDE]
widgetStyle=Breeze
[Icons]
Theme=breeze
[Wallpaper]
Image=file:///etc/az-wallpaper
'';
# Lock-screen footer (rendered by kscreenlocker_greet).
"xdg/kscreenlockerrc".text = ''
[Greeter]
Logo=${config.az.tc.branding.logo}
Footer=${config.az.tc.branding.company} · ${config.networking.hostName} · IT-Hotline: ${config.az.tc.branding.hotline}
'';
};
};
# Property-of-footer via SDDM theme config. Lightweight: we don't
# ship a full custom SDDM theme — we just set the footer string that
# the default Breeze SDDM theme shows. SDDM's settings module wants
# strings (INI atoms), not Nix paths, so we use absolute paths.
services.displayManager.sddm.settings.Theme = {
Wallpaper = "${config.az.tc.branding.wallpaper}";
Logo = "${config.az.tc.branding.logo}";
};
};
}
-11
View File
@@ -1,11 +0,0 @@
# roles/thin-client/session/default.nix
#
# KDE Plasma 6 session on Wayland, SDDM, locale/timezone already set in
# the top-level role. Submodules pull in branding, audio, fonts.
{config, lib, pkgs, ...}: {
imports = [
./desktop.nix
./branding.nix
./audio.nix
];
}
-80
View File
@@ -1,80 +0,0 @@
# roles/thin-client/session/desktop.nix
#
# KDE Plasma 6 on Wayland, SDDM display manager.
# Q6 decisions: Wayland, no autologin, Full-HD uniform monitors.
{
config,
lib,
pkgs,
...
}: let
inherit (lib) mkIf;
cfg = config.az.tc;
in {
config = mkIf cfg.enable {
# Xserver base — required for keyboard config even on Wayland.
services.xserver.enable = true;
services.displayManager = {
sddm = {
enable = true;
wayland.enable = true;
autoNumlock = true;
settings = {
Theme = {
# Branded theme set in branding.nix
CursorTheme = "breeze_cursors";
};
Autologin = {
# Explicitly disabled — multi-user machine.
User = "";
Session = "";
};
};
};
defaultSession = "plasma";
};
services.desktopManager.plasma6 = {
enable = true;
};
# Wayland portal for screen capture (RustDesk + OBS).
xdg.portal = {
enable = true;
extraPortals = [
pkgs.kdePackages.xdg-desktop-portal-kde
];
config.common.default = ["kde"];
};
# PipeWire for audio (RustDesk/OBS/Chrome all consume it).
security.rtkit.enable = true;
# German keyboard layout
services.xserver.xkb = {
layout = "de";
variant = "";
options = "eurosign:e";
};
# Essential KDE/PIM utilities users expect
environment.plasma6.excludePackages = with pkgs.kdePackages; [
konsole
elisa
khelpcenter
];
environment.systemPackages = with pkgs; [
# Thin-client baseline utilities
kitty
ark
kdePackages.kcalc
kdePackages.spectacle
kdePackages.filelight
];
# Firewall UI helper (KDE) — optional but useful for support staff
# networking.firewall is configured in network/firewall.nix
};
}
-64
View File
@@ -1,64 +0,0 @@
let
#SYSTEMS
AZ-CLD-1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIItSijmU5YwcJcoshtmYxpxBaVA4TPaCMk23ws7KDkAH";
AZ-LT-NIX = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIg/nFOPx763xIbepPsdYRE49R7HwvikXhLF/iPgH1Jh";
AZ-PRM-1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID6gl9maUQ99I4t8mCAdfUw6lrA9NYx2EbwqGOmKts+l";
# ── Thin Client Fleet Hosts ───────────────────────────────────────
# SSH host keys (ed25519) are generated on first boot of each host.
# After first boot, retrieve via: ssh-keyscan -t ed25519 AZ-TC-NN.netbird
# and paste the public key here. Until then, the corresponding .age
# secrets can't be decrypted by the host.
AZ-TC-01 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfDCRFvGkduqaxKMQkCN0hiJ+096WBBSvJBf5TprNgE";
# AZ-TC-02 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5PLACEHOLDER-REPLACE-WITH-REAL-HOSTKEY-02";
# AZ-TC-03 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5PLACEHOLDER-REPLACE-WITH-REAL-HOSTKEY-03";
#USERS
sascha.koenig = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZbg/Z9mnflXuLahGY8WOSBMqbgeqVIkIwRkquys1Ml";
jannik.mueller = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPvZazSuIoWoRWhkAqQDMLeurxVUyy1MTllp1wfw1tzq";
users = [sascha.koenig jannik.mueller];
systems = [AZ-CLD-1 AZ-LT-NIX AZ-PRM-1];
thinClients = [AZ-TC-01];
in {
"secrets/elevenlabs-key.age".publicKeys = systems ++ users;
"secrets/exa-key.age".publicKeys = systems ++ users;
"secrets/kestractl-env.age".publicKeys = systems ++ users;
"secrets/outline-key.age".publicKeys = systems ++ users;
"secrets/ref-key.age".publicKeys = systems ++ users;
"secrets/sascha.koenig-secrets.age".publicKeys = [AZ-LT-NIX sascha.koenig];
# ── Per-host Thin Client secrets ──────────────────────────────────
# Each Thin Client needs:
# - <host>-krb5-keytab.age — AD machine keytab
# - <host>-netbird-setupkey.age — NetBird setup key
# - <host>-wifi-client-cert.age — WiFi EAP-TLS client cert (PEM, key+cert)
# - <host>-rustdesk-password.age — RustDesk permanent password
# Plus a shared one for all thin clients:
# - rustdesk-psk.age — RustDesk pre-shared key
#
# Create with: agenix -e secrets/AZ-TC-01-krb5-keytab.age
# Shared RustDesk PSK — readable by all thin clients
"secrets/rustdesk-psk.age".publicKeys = thinClients ++ users;
# Shared Snipe-IT API token — same token for all thin clients (they
# only check themselves in, not manage assets).
"secrets/snipeit-api-token.age".publicKeys = thinClients ++ users;
# Per-host Thin Client secrets
"secrets/AZ-TC-01-krb5-keytab.age".publicKeys = [AZ-TC-01] ++ users;
"secrets/AZ-TC-01-netbird-setupkey.age".publicKeys = [AZ-TC-01] ++ users;
"secrets/AZ-TC-01-wifi-client-cert.age".publicKeys = [AZ-TC-01] ++ users;
"secrets/AZ-TC-01-rustdesk-password.age".publicKeys = [AZ-TC-01] ++ users;
# "secrets/AZ-TC-02-krb5-keytab.age".publicKeys = [AZ-TC-02] ++ users;
# "secrets/AZ-TC-02-netbird-setupkey.age".publicKeys = [AZ-TC-02] ++ users;
# "secrets/AZ-TC-02-wifi-client-cert.age".publicKeys = [AZ-TC-02] ++ users;
# "secrets/AZ-TC-02-rustdesk-password.age".publicKeys = [AZ-TC-02] ++ users;
# "secrets/AZ-TC-03-krb5-keytab.age".publicKeys = [AZ-TC-03] ++ users;
# "secrets/AZ-TC-03-netbird-setupkey.age".publicKeys = [AZ-TC-03] ++ users;
# "secrets/AZ-TC-03-wifi-client-cert.age".publicKeys = [AZ-TC-03] ++ users;
# "secrets/AZ-TC-03-rustdesk-password.age".publicKeys = [AZ-TC-03] ++ users;
}
-42
View File
@@ -1,42 +0,0 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFZWN05LdyBpSGs5
amgyV0tCZkEyUHd3MUNEVzc4SERiTWdpMDM4amtsMEl5VXNOZUQ4CklXMmJNWEw3
V2ovc1JPaktPdWs2NDlFYWxJelZnZk9PV0FtNUJOMG8yYmMKLT4gc3NoLWVkMjU1
MTkgQ1NNeWhnIHIvelYvaFF5TXVoaTRHSVg5ZTNmb1pZMTNON1RPOUlGZ0I4TlBt
SjJ1ZzAKREp5dnZmVEdoSmpJTVNOb2RhWFR1MlVSbTNOOE5XZFNyWjlFdkcwTVE3
NAotPiBzc2gtZWQyNTUxOSBvWUxjRncgbzZiaDFmOXZKNkp2Snd3eGRDS1ZKdk8r
aDhvUVhMSU9odFBpUXpYNDF6YwpjTmJjYkJna0tIdkRsdHFNaThYaU9XQ0RyVjJD
RGFTM3hVMEFQU0V2azFvCi0+IC1+MEB8LFM5LWdyZWFzZSBQRlN8bSBfcCBbWiB2
UwpiY05OcE5jaE93bThwTGxraXFRaW1RdUUrQTd2Ymgyd3NxMU9IamxiWUlFSWMx
MGZLNEVzZDBrMEVmanlzSVR5CkNWQlFlZjlGL2N2ZHFJSVNQZHNpR05CT3pDTG5D
TEEKLS0tIDRZSVVKRWVqUG1uUHpXUUV6ZWpTbWpZNVF5MWF4dFR5OEliWm9KSnpQ
WTQKWklnQ9B/Ig4adbUCM+oE7kIVH5MBoiZjdiXmNj4FukIRpDmgEvzddINntdG8
B/GUs7wWVIh1j9u4DL1/8Vok7Vp7nKKDrGVcwONVdPusIn0z1pR+0MXRy4puDthr
SnI79KN/rZotMTM60zfPbPKmw51rGsA1WofsudXhNo3HgCLgM1n9DZCeuXTOWWzr
vULWJRuFkbEL5XLwDBUz0xI7QFJmGOnMvHh7HLjyQX7akuPOSzqN8R1FIm+Ol5uY
4BL3t7Xfj9Iun3Jzb7phahFHZADs4oA+YguoP4yF99+TOwBVw7eV16sNjM/caEyt
3Rul28jP6VhoglLYUkYrDYRPNpQsQU7ZkB9MuW/96RIoA7XL6YqlM/YuhfGlA8VO
CvKcfaYNMMyZT8r8gR40MEK15JpComGOvhJu8JHSvvinpqvwbeYOAU7f/gj4oorQ
IFEGfEewBRe94vhHnkR9GJqznHRVTQiOENiBJi33cz3ZWXCZGwF1zsziMLy8/HaQ
/wCyDeRbAUo+zaUeESPIV3k346cmrn90tMA7ChU5Amgh0Dr34ry4jqW9m99p9viY
KlgAD1Fe3nQ+LRR3lyQOdSe2HiHN4zq3JP/zu6Kvb3FoNMpCrRVJ7ZkjHog+8lAA
fauw7cRK1ebUKvEVJU3FHqqRLSLre8eEgYoG1jOlee2Et2Oa9euHxwUJuMV9MaiP
hT0g1eLyW5TDllKSVYYDt4yv3Hd+p33nhHi6wz8gVBpgNTaqPtl76MvrpuWDzSeZ
VZSXj4mTT2hos8JIwnlqmM5wTm/U1RzEEcT1FoqOzDYzya9Cq8O5dG1AjomzOKpv
bmfBV+uLqtUTO80sTPPtUeOd7t7AUekXk25Hh8wk7k8Vbjx1PlW9rT1w4KziGvqb
ni2VxzhawmujsY/EFxu7pDYRJ1bDBv4Lus4aM2ZEIGDbIKcvatgaKvtdLmPYq0dC
UkLmiNoulEQmF38GMZ1UgVW67RPUFMRMAp5ZfIl+5KrorcmKqmxmmIClftIgwBkR
nV1BQgdMFr5tnLSLqQePahRyEormv7TRUIrHnGWMoTjHEsZNT5Oc/vn66UnmIY5D
S+YO1ayEmWCRaZ63+ITnc1aKLEQR9BRS6qIN7ZG4EB1X61D4v2M5lFvo/dCHFclC
EWWE0ncVBsHXqDedxDnWK+7ceybo8KbLl+uAZDX+RLDtJVibc/CSdhfcaqSAIleI
Ss+D2amcqp37s5SScI7sPqhmilWmJSHRP7y0sGMkRu9qyoRrA31H1pXCk/9yWyXT
iakjlXd3Dz1YLsAx3VLkvsD4m5zShIiprBA1nuiHSOBPcmQoS5ZljhpKKZT2OPG+
zcWObJso5ndjoW/Qnkr1FGxIjFPkXDt0buVZ6B7Q3JbJvBg66TzA5iSIkX78Fad2
zB3OtVBfpq81V6XZLe825/3osV0HQBQ84xTDMU2wXlaaV6IKUBcqQBcBeDyZL6xH
299Wg5E1z/HXTG3uYfbe3vV+HAzgzPvB5qI1usMPj0Bvud8iSjflEMSPLAZOkufl
dRcKp6wKLDT6+8S8Wpty0n0pNL43cl3RSVzxD4PaLiss72HlmjUmGjPK3bdxm0rz
6/WpB9VEzbv8WcRJSZl7FE1vP2h32qCh3+0w4dUqIa/A1am7RMgilbi/EvEdREVc
gsL4sOj/gPi6hqkYJxhBsZ7RrIuhyQvXEfcUj/57NlOrWTiFaTM4JqkIAIxmACCK
S8VNIjQEN9QADNOGxjxXXzkUKmp0GmMn53vzO02zRBdX3Lrqzgt4N7TNePH/XIb7
ncy2WsNR41UUXiJByEKlHUZzvdhSaQb7cY3WtabfyGF3MF4=
-----END AGE ENCRYPTED FILE-----

Some files were not shown because too many files have changed in this diff Show More