Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c25fc5d41 | ||
|
|
09e70769b1 | ||
|
|
4a24558981 | ||
|
|
828399e379 | ||
|
|
c71aba5f64 | ||
|
|
b33eeb71b8 | ||
|
|
0cc0b5064d | ||
|
|
98dc2917e8 | ||
|
|
ea0c4ccf22 | ||
|
|
f8733b6a86 | ||
|
|
45ffea03f9 | ||
|
|
76162a6366 | ||
|
|
f59644b8e4 | ||
|
|
479d6fdeea | ||
|
|
7306caa58c | ||
|
|
040a25ca7e | ||
|
|
e02b90ad14 | ||
|
|
fc5f70c166 | ||
|
|
d56ef56dcd | ||
|
|
06815b1bd8 | ||
|
|
91e2814629 | ||
|
|
1804613a44 | ||
|
|
7dc953f5b5 | ||
|
|
8e921a05d8 |
@@ -0,0 +1,80 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Beads"
|
||||
short_description: "Project task tracking with bd"
|
||||
default_prompt: "Use $beads to inspect ready work and manage durable project tasks."
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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* ⚡
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/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 ---
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/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 ---
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/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 ---
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/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 ---
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/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 ---
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"database": "dolt",
|
||||
"backend": "dolt",
|
||||
"dolt_mode": "embedded",
|
||||
"dolt_database": "AZ_NIX_CLIENTS",
|
||||
"project_id": "eaeef29c-e367-4856-b22a-27a3b54a949a"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"command": "bd prime --hook-json",
|
||||
"type": "command"
|
||||
}
|
||||
],
|
||||
"matcher": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[features]
|
||||
hooks = true
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Activate the devshell from the Nix flake
|
||||
# This loads all tools and environment variables defined in flake.nix
|
||||
|
||||
use flake
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
# Use bd merge for beads JSONL files
|
||||
.beads/issues.jsonl merge=beads
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# 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/
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/nix/store/7jh7qhfs4gwcrzbpc3p0w9scdqr8vrzb-source/rules
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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 -->
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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
@@ -0,0 +1,47 @@
|
||||
# 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
|
||||
@@ -1,7 +0,0 @@
|
||||
This repository is being used as a Dolt remote.
|
||||
|
||||
ref=refs/dolt/data
|
||||
|
||||
head=9e0218094766fa36c5298c84228fb41f50b37d1e
|
||||
|
||||
timestamp=2026-08-31T16:24:03Z
|
||||
@@ -0,0 +1,634 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
{
|
||||
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
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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?
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
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"];
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# 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;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
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];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
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 - -"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{...}: let
|
||||
serviceName = "n8n";
|
||||
in {
|
||||
services.${serviceName} = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
};
|
||||
systemd.services.n8n = {
|
||||
environment = {
|
||||
N8N_SECURE_COOKIE = "false";
|
||||
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS = "false";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{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";
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{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";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
security.rtkit.enable = true;
|
||||
services.pipewire = {
|
||||
enable = true;
|
||||
alsa.enable = true;
|
||||
alsa.support32Bit = true;
|
||||
pulse.enable = true;
|
||||
jack.enable = false;
|
||||
wireplumber.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{pkgs, ...}: {
|
||||
services.udev.extraRules = ''
|
||||
SUBSYSTEM=="usb", MODE="0666"
|
||||
'';
|
||||
environment.systemPackages = with pkgs; [
|
||||
zsa-udev-rules
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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";
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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";
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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";
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
imports = [
|
||||
./flatpak.nix
|
||||
./ollama.nix
|
||||
./podman.nix
|
||||
./virtualisation.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
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 = "*";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
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"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
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
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{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
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
imports = [
|
||||
./jannik.mueller.nix
|
||||
./sascha.koenig.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
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];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
# 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 []
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{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"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# 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];
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
# {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;
|
||||
# })
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{pkgs, ...}: {
|
||||
# Define your custom packages here
|
||||
# pomodoro-timer = pkgs.callPackage ./pomodoro-timer {};
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
# 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
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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"
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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")
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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 -"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
# 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.";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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}";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
# 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}"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
-----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.
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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).
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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.)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
# 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"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# roles/thin-client/peripherals/default.nix
|
||||
{config, lib, ...}: {
|
||||
imports = [
|
||||
./printing.nix
|
||||
./smb-mounts.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
# 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.)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 388 B |
@@ -0,0 +1,12 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 626 B |
@@ -0,0 +1,25 @@
|
||||
# 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
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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}";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
-----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
Reference in New Issue
Block a user