Compare commits

..

25 Commits

Author SHA1 Message Date
c842b4a398 Streamline AI docs, add skills, enforce symlink-everything policy
- Add .claude/skills/: homelab-context (auto-loads key facts each session),
  homelab-status (/homelab-status command), deploy-config (symlink setup guide)
- Remove AGENTS.md and ai/sessions/: superseded by plan mode + skill system
- Remove 4 obsolete session commands (session-start/list/switch, reload-instructions)
- Rewrite CLAUDE.md: remove duplicate content, enforce symlink policy, clarify sudo pattern
- Trim docs/services.md from 946 to ~230 lines: remove planning-era content,
  keep install steps and current status for migration reference
- Strip stale "sudo cp" deploy header from ssh-honeypot.service (now symlinked to repo)
- Update TODO.md: mark NAS migration and symlink tasks done, add jellyfin upgrade warning
2026-03-01 23:45:18 +01:00
14cc6aa50d Fix user guid for gitea as well 2026-03-01 22:59:19 +01:00
ee0c7035d3 Move Gitea and Nextcloud storage to NAS
- Gitea: data volume moved from local ./data to /mnt/nas/docker-data/gitea/data
- Nextcloud app: data volume moved from Docker named volume to /mnt/nas/docker-data/nextcloud/app-data
- Nextcloud DB: moved to /mnt/nas/docker-data/nextcloud/db-data with NFS compatibility fixes:
  - user: 1024:100 to match NAS file ownership and avoid chown failures
  - innodb_use_native_aio=0 (NFS requirement)
  - innodb_flush_method=O_DIRECT (better NFS performance)
  - MARIADB_INITDB_SKIP_TZINFO=1 (avoid tz table load on NFS)
- Removed named Docker volumes nextcloud_db and nextcloud_data (replaced by NAS mounts)
2026-03-01 22:45:51 +01:00
ea1c71f0f0 Pin Docker image versions and fix config drift
- Pin all :latest tags to specific versions for reproducibility:
  jellyfin→10.11.6, gitea→1.24, portainer→2.39.0, qbittorrent→5.1.4
- Fix nextcloud config drift: repo had :29, live was running :32
- Fix qbittorrent: remove user: directive (breaks s6-overlay init)
- Fix portainer: remove healthcheck (distroless image has no shell)
2026-02-28 00:28:17 +01:00
059daa77aa Fix SSL certificate renewal for Let's Encrypt
Allow ACME challenge requests before HTTPS redirect:
- Add location block for /.well-known/acme-challenge/
- Prevent immediate redirect that was blocking Let's Encrypt verification
- Fixes 'Timeout during connect' errors during certbot renewal
2025-11-15 22:30:01 +01:00
9aa881d895 Document voice assistant TTS service status
Mark TTS functionality as disabled due to onnxruntime removal
(freed 1.2GB disk space during cleanup)
2025-10-11 18:25:08 +02:00
ab4a4cfc9c Add comprehensive backup system documentation
Document complete backup strategy covering:
- Setup and initialization procedures
- What gets backed up (and what doesn't)
- Multiple restore scenarios (full reinstall, rollback, specific volumes)
- Retention policy explanation
- Troubleshooting and security notes
2025-10-11 18:24:58 +02:00
646d3e59bc Add comprehensive system backup scripts
Implement full system backup with Restic including:
- Docker volume exports (Gitea, Jellyfin, Nextcloud, Portainer)
- System configuration backup (/etc/)
- Package list exports (pacman explicit, all, AUR)
- Automated retention policy (7 daily, 4 weekly, 3 monthly, 1 yearly)
- Separate temporary directories for Docker and system data
2025-10-11 18:24:51 +02:00
ad7270aa74 Add Restic backup repository initialization script
Initialize encrypted backup repository on NAS with:
- Auto-install of Restic if needed
- Secure password generation and storage
- Repository initialization with AES-256 encryption
2025-10-11 18:24:41 +02:00
7e64557f0b Add Nextcloud reverse proxy configuration to nginx 2025-10-11 17:25:56 +02:00
acc30e7c3d Enable Nextcloud link on landing page 2025-10-11 17:25:44 +02:00
3d57327146 Re-enable Nextcloud service with Docker configuration 2025-10-11 17:25:31 +02:00
d0727bd8a7 Document dockerization decisions and status
- Mark dockerization task as complete
- Document reasons for non-dockerized services:
  - Glances/Netdata: Need full system access for monitoring
  - Syncthing: Docker permission issues with config directory
  - Nginx: Reverse proxy requires system integration
- Update service status: Copyparty now dockerized
- Remove Cockpit references (removed from system)
- Update VNC documentation to reflect temporary usage pattern
2025-10-06 22:45:58 +02:00
7eaf16f53e Update symlink setup script for all Docker services
- Add automatic directory creation in create_symlink function
- Include copyparty, portainer, and qbittorrent configs
- Add landing page symlink for easier updates
- Update verification commands to include all services
2025-10-06 22:45:36 +02:00
bab97f7166 Add Copyparty Docker configuration
- Replace config file with command-line arguments for Docker compatibility
- Enable file search, deduplication, and partial upload features
- Configure reverse proxy support with xff-src and rproxy flags
- Add password database integration with chpw support
- Map all NAS volumes with appropriate permissions
- Fix health check to use /files/ path
- Remove obsolete copyparty.conf (incompatible with Docker image)
2025-10-06 22:43:58 +02:00
6894c5f175 Update geoip-shell documentation with implementation details
- Document interactive installation process
- Add complete EU country whitelist configuration
- Include setup commands and wizard prompts
- Clarify that geoip-shell was used instead of manual iptables setup
2025-10-06 20:40:27 +02:00
6761c8903d Add Docker configuration symlink setup script
- Automates symlinking of docker-compose.yml files from repo to /opt/docker
- Includes Gitea, Jellyfin, qBittorrent, Portainer
- Symlinks daemon.json to /etc/docker
- Creates timestamped backups before replacing files
- Eliminates need for manual config copying
2025-10-06 20:40:11 +02:00
a8bbf5ea4a Disable Nextcloud service
- Renamed docker-compose.yml to .disabled
- Service can be re-enabled by renaming file back to .yml
- Nextcloud not currently needed with Copyparty in place
2025-10-06 20:39:54 +02:00
fefdc14398 Add Portainer docker-compose configuration
- Web UI for Docker container management
- Bound to localhost (reverse proxy recommended)
- Uses named volume for persistent data
- Configured for Europe/Budapest timezone
- Resource limits and health checks included
2025-10-06 20:39:33 +02:00
802d275d6e Fix Docker daemon config: Remove invalid JSON comments
- Removed comments from daemon.json (JSON doesn't support comments)
- Synced with deployed working version
- Maintains minimal working configuration with logging only
2025-10-06 20:39:21 +02:00
dc16d0302d Add installers folder to copyparty configuration
- New /installers volume for game installers and ISO files
- Accessible to both guest and hoborg users
- Complements existing torrent categorization structure
2025-10-06 20:39:01 +02:00
64a43c8a96 docs: Add CLAUDE.md and permanent ban script
- Add CLAUDE.md with AI assistant configuration
- Add scripts/permanent-ban-repeat-offenders.sh for automated permanent banning
- Script automatically detects and permanently bans IPs banned >4 times by fail2ban
- Integrates with iptables and geoip-shell for comprehensive security
2025-09-17 01:09:41 +02:00
43cfd25798 docs: Update cron schedules to match actual configuration
- Correct permanent ban script cron schedule: 2:00 AM daily (not every 6 hours)
- Correct geoip-shell update cron schedule: 4:08 AM daily (not 4:18 AM)
- Add geoip-shell persistence cron job that runs on reboot
2025-09-17 01:08:51 +02:00
00f4fcbc1c docs: Add comprehensive geoip-shell and permanent ban documentation
- Add docs/geoip-blocking.md with complete geoip-shell setup documentation
- Update README.md to include geoip blocking in goals, status, and documentation structure
- Update docs/network-security.md with geoip blocking and permanent ban sections
- Mark geoip blocking task as completed in TODO.md
- Document permanent-ban-repeat-offenders.sh script and its cron job
2025-09-17 01:08:13 +02:00
31a9568fd4 Add gitea security notes 2025-09-13 20:51:53 +02:00
29 changed files with 1763 additions and 1211 deletions

View File

@@ -0,0 +1,52 @@
---
name: deploy-config
description: Set up symlinks from system config locations to this repo. Use when setting up on a new machine, or when a config file is found to be a real file instead of a symlink (config drift). Not for routine config changes — just edit the repo file directly.
disable-model-invocation: true
allowed-tools: Write, Bash(chmod *), Bash(diff *), Bash(ls *)
---
All system configs should be symlinked to this repo. Editing the repo file IS editing the live config.
## Symlink Map
| System location | Repo source |
|---|---|
| `/etc/nginx/sites-available/homelab` | `config/nginx/homelab.conf` |
| `/var/www/homelab/index.html` | `config/www/index.html` |
| `/etc/systemd/system/copyparty.service` | `config/systemd/copyparty.service` |
| `/etc/systemd/system/glances-web.service` | `config/systemd/glances-web.service` |
| `/etc/systemd/system/ssh-honeypot.service` | `config/systemd/ssh-honeypot.service` |
| `/opt/docker/<service>/docker-compose.yml` | `config/docker/<service>/docker-compose.yml` |
## When to use this skill
- **New machine setup**: Create all symlinks from scratch
- **Drift detected**: A system file is a real file instead of a symlink — replace it with a symlink
- **New config added to repo**: Create the initial symlink for it
## How to create/fix a symlink
Generate a script in `scripts/tmp/symlink-<name>.sh`:
```bash
#!/bin/bash
set -e
REPO="/home/hoborg/homelab/<repo-path>"
SYSTEM="<system-path>"
# Back up if it's a real file (not already a symlink)
[ ! -L "$SYSTEM" ] && cp "$SYSTEM" "${SYSTEM}.backup.$(date +%Y%m%d)" && echo "Backup created"
ln -sf "$REPO" "$SYSTEM" && echo "Symlink created"
# Add post-link steps here (e.g. nginx -t && systemctl reload nginx, systemctl daemon-reload)
```
Tell the user to run: `sudo bash ~/homelab/scripts/tmp/symlink-<name>.sh`
## Routine config changes
Just edit `config/<path>` in the repo. The symlink means it's already live. Then:
- Nginx: `sudo nginx -t && sudo systemctl reload nginx`
- Systemd unit: `sudo systemctl daemon-reload && sudo systemctl restart <service>`
- Docker: `cd /opt/docker/<service> && docker compose restart`
- www/index.html: no action needed (served directly)

View File

@@ -0,0 +1,98 @@
---
name: homelab-context
description: Background reference for the homelab repo. Auto-loads key facts about services, ports, paths, and operational rules when working in this project.
user-invocable: false
---
## Architecture
- **Domain**: ak-homelab.duckdns.org (DuckDNS)
- **Static IP**: 192.168.0.100 (interface: enp4s0)
- **SSH**: port 2222 (system), port 2223 (Gitea git)
- **NAS**: 192.168.0.101, mounted at `/mnt/nas/`
## Services & Ports
| Service | Port | Type | Path |
|-------------|-------|----------|-----------------|
| Nginx | 80/443| systemd | reverse proxy |
| Gitea | 3000 | Docker | /gitea/ |
| Jellyfin | 8096 | Docker | /media/ |
| Copyparty | 8082 | systemd | /files/ |
| Netdata | 19999 | Docker | /netdata/ |
| Portainer | 9000 | Docker | /portainer/ |
| qBittorrent | 8080 | Docker | /qbt/ |
## Key Paths
All configs are **symlinked** from system locations to this repo — editing the repo file is editing the live config:
| System location | Repo source |
|---|---|
| `/opt/docker/<service>/docker-compose.yml` | `config/docker/<service>/docker-compose.yml` |
| `/etc/nginx/sites-available/homelab` | `config/nginx/homelab.conf` |
| `/var/www/homelab/index.html` | `config/www/index.html` |
| `/etc/systemd/system/copyparty.service` | `config/systemd/copyparty.service` |
| `/etc/systemd/system/glances-web.service` | `config/systemd/glances-web.service` |
| `/etc/systemd/system/ssh-honeypot.service` | `config/systemd/ssh-honeypot.service` |
- NAS docker data: `/mnt/nas/docker-data/<service>/`
## Critical Rules
### Docker UID for NAS-mounted volumes
Services with data on NAS (`/mnt/nas/`) must use `USER_UID=1024 USER_GID=100` to match NAS file ownership. Services with local storage use `1000:1000`.
### Docker config changes
Edit `config/docker/<service>/docker-compose.yml` in repo → restart container. No copy needed (symlinked).
### After editing a config in repo
- **Docker**: `cd /opt/docker/<service> && docker compose restart`
- **Nginx**: `sudo nginx -t && sudo systemctl reload nginx`
- **Systemd unit**: `sudo systemctl daemon-reload && sudo systemctl restart <service>`
- **www/index.html**: no action needed
### Sudo scripts
Never run sudo directly. Generate a script in `scripts/tmp/` and run `sudo bash ~/homelab/scripts/tmp/<script>.sh`.
The sudoers rule `/etc/sudoers.d/homelab-scripts` grants NOPASSWD for `scripts/tmp/*` — no password needed.
### s6-overlay containers (Gitea)
Do NOT set `user:` directive in docker-compose. Use `USER_UID`/`USER_GID` env vars instead — s6-overlay needs root to start, then drops privileges.
### Never copy any config files
All configs are symlinked. Copying a file breaks the link and causes drift from the repo.
## NAS Services Currently Using 1024:100
- Gitea (`/mnt/nas/docker-data/gitea/data`)
- Nextcloud app + db (`/mnt/nas/docker-data/nextcloud/`)
## Management Quick Reference
```bash
# Docker service (from anywhere)
cd /opt/docker/<service> && docker compose logs -f
cd /opt/docker/<service> && docker compose restart
cd /opt/docker/<service> && docker compose down && docker compose up -d
# Systemd services
sudo systemctl status nginx copyparty netdata
sudo systemctl restart nginx
# Nginx
sudo nginx -t && sudo systemctl reload nginx
# NAS mount check
mountpoint /mnt/nas && echo mounted
```
## Keeping Skills Up To Date
After any change to service configuration, ports, paths, Docker UIDs, or operational rules:
- Update this skill file (`homelab-context/SKILL.md`) to reflect the new state
- Update `homelab-status/SKILL.md` if services are added or removed
- This ensures future sessions always have accurate context without re-reading files
## Docs Reference
- Full service install steps (for migration): `docs/services.md`
- Network & security: `docs/network-security.md`
- Troubleshooting: `docs/troubleshooting/`

View File

@@ -0,0 +1,15 @@
---
name: homelab-status
description: Check status of all homelab services at once.
disable-model-invocation: true
allowed-tools: Bash(docker *), Bash(systemctl *), Bash(mountpoint *), Bash(nginx *)
---
Check and report status of all homelab services:
1. Run `docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"` to show all containers
2. Run `systemctl is-active nginx copyparty netdata` to show systemd service states
3. Run `mountpoint /mnt/nas` to confirm NAS is mounted
4. Run `sudo nginx -t` to verify nginx config is valid
5. Report any services that are stopped, failed, or unhealthy
6. Note any containers not running that should be (gitea, jellyfin, portainer, qbittorrent, nextcloud)

View File

@@ -1,76 +0,0 @@
# AI Agent Instructions and Restrictions
## CRITICAL SECURITY RESTRICTIONS
### ❌ SUDO COMMAND PROHIBITION
**NEVER, UNDER ANY CIRCUMSTANCES, RUN SUDO COMMANDS**
The AI agent MUST NOT execute any commands that require elevated privileges:
-`sudo` commands
-`su` commands
- ❌ Commands that modify system files directly
- ❌ Commands that require root privileges
### ✅ APPROVED ALTERNATIVES
Instead of running sudo commands, the AI should:
1. **Create scripts** that the user can run with `sudo -A`
2. **Document commands** for the user to execute manually
3. **Explain what needs to be done** and why
4. **Provide step-by-step instructions** for the user
### EXAMPLES
**❌ WRONG:**
```bash
sudo systemctl restart nginx
sudo chmod 755 /etc/nginx/conf.d/
```
**✅ CORRECT:**
```bash
# Create a script for the user to run
echo "systemctl restart nginx" > /tmp/restart-nginx.sh
chmod +x /tmp/restart-nginx.sh
# Then tell the user:
# "Run: sudo -A /tmp/restart-nginx.sh"
```
### RATIONALE
- User explicitly denied AI access to sudo
- Security best practice: AI should not have root privileges
- User prefers manual control over system changes
- Prevents accidental system modifications
### VERIFICATION
This restriction has been tested and verified:
- ✅ AI cannot run sudo commands via bash tool
- ✅ AI will create scripts instead
- ✅ User maintains full control over privileged operations
## OTHER OPERATIONAL GUIDELINES
### File Operations
- ✅ Read files in user directories
- ✅ Write files in user directories
- ✅ Create scripts in `/tmp` or user directories
- ❌ Modify system configuration files directly
### Network Operations
- ✅ Check network status with unprivileged commands
- ✅ Test connectivity
- ❌ Modify firewall rules directly
- ❌ Bind to privileged ports (< 1024)
### Service Management
- Start/stop/restart system services directly
- Check service status with unprivileged commands
- Create systemd service files for user to deploy
---
**REMEMBER: When in doubt, create a script and let the user run it with sudo.**

230
CLAUDE.md
View File

@@ -4,192 +4,106 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
This is a personal homelab setup repository documenting the migration from cloud services to self-hosted solutions on a ThinkPad running Arch Linux. The project uses Docker for containerized services behind an Nginx reverse proxy. Personal homelab on a ThinkPad running Arch Linux. Docker services behind an Nginx reverse proxy. Full context is in `.claude/skills/homelab-context/SKILL.md`.
## Architecture ## Architecture
### Network Infrastructure - **Domain**: ak-homelab.duckdns.org (DuckDNS)
- **Domain**: ak-homelab.duckdns.org (DuckDNS dynamic DNS) - **Static IP**: 192.168.0.100 (interface: enp4s0)
- **Static IP**: 192.168.0.100 (dual interface: ethernet enp4s0, WiFi wlp1s0) - **NAS**: 192.168.0.101, mounted at `/mnt/nas/`
- **SSH**: Custom port 2222 for system access - **Reverse Proxy**: Nginx → path-based routing to services
- **Reverse Proxy**: Nginx for path-based routing to services
## Configuration Management
All configs are version-controlled in `config/`. Docker compose files are **always symlinked** — never copied:
### Service Architecture
``` ```
Internet → Router → Nginx (80/443) → Services /opt/docker/<service>/docker-compose.yml → ~/homelab/config/docker/<service>/docker-compose.yml
→ SSH (2222) → System
→ Gitea SSH (2223) → Git operations
``` ```
**Current Services:** Editing the repo file is editing the live config. Restart the container to apply changes.
- Gitea Git server: Docker container on port 3000, accessible via `/gitea/` path
- Copyparty file server: Systemd service on port 8082, accessible via `/files/` path with WebDAV support
- Jellyfin media server: Docker container on port 8096, accessible via `/media/` path
- Nginx: Reverse proxy routing to `ak-homelab.duckdns.org/servicename/`
### Configuration Management All configs — Docker, nginx, systemd units, www — are symlinked to this repo. Editing a file in `config/` is editing the live config. Use `/deploy-config` only on a new machine to create symlinks from scratch, or if drift is detected (a real file exists instead of a symlink).
All configurations are version controlled in the `config/` directory: ## Sudo Access
- `config/docker/gitea/`: Gitea container setup and deployment scripts
- `config/docker/jellyfin/`: Jellyfin media server configuration
- `config/nginx/`: Reverse proxy configurations with SSL and WebDAV support
- `config/scripts/`: Utility scripts for system maintenance
- `~/.config/copyparty/`: Copyparty file server configuration
Configuration files include deployment instructions in comments showing target locations (e.g., `/opt/docker/gitea/`, `/etc/nginx/sites-available/`). A sudoers rule at `/etc/sudoers.d/homelab-scripts` grants NOPASSWD for scripts in `scripts/tmp/`:
```
hoborg ALL=(ALL) NOPASSWD: /bin/bash /home/hoborg/homelab/scripts/tmp/*
```
Scripts generated there can be run with `sudo bash ~/homelab/scripts/tmp/<script>.sh` — no password needed.
Never run sudo commands directly. Always create a script in `scripts/tmp/`.
## Docker Guidelines
- **NAS-mounted volumes** (`/mnt/nas/`): use `USER_UID=1024 USER_GID=100` to match NAS file ownership
- **Local volumes**: use `USER_UID=1000 USER_GID=1000`
- **s6-overlay containers** (Gitea): do NOT set `user:` directive — use `USER_UID`/`USER_GID` env vars only
- **Never copy docker-compose files** — they are symlinked; copying breaks the link and causes config drift
## Key Commands ## Key Commands
### Docker Service Management ### Docker
```bash ```bash
# Gitea operations (from /opt/docker/gitea/) cd /opt/docker/<service>
docker-compose logs gitea # View logs docker compose logs -f # View logs
docker-compose down # Stop docker compose restart # Restart
docker-compose up -d # Start docker compose down && docker compose up -d # Full restart
docker-compose pull && docker-compose up -d # Update
# Jellyfin operations (from /opt/docker/jellyfin/)
docker-compose logs jellyfin # View logs
docker-compose down # Stop
docker-compose up -d # Start
docker-compose pull && docker-compose up -d # Update
# Deploy from repo
sudo cp config/docker/gitea/docker-compose.yml /opt/docker/gitea/
sudo cp config/docker/jellyfin/docker-compose.yml /opt/docker/jellyfin/
``` ```
### Copyparty File Server Management ### Nginx
```bash ```bash
# Service operations
sudo systemctl status copyparty # Check status
sudo systemctl start copyparty # Start service
sudo systemctl stop copyparty # Stop service
sudo systemctl restart copyparty # Restart (reload config)
# Configuration
sudo cp ~/.config/copyparty/copyparty.conf ~/.config/copyparty/copyparty.conf.backup
# Edit config and restart service to reload
```
### Nginx Operations
```bash
# Deploy configuration
sudo cp config/nginx/homelab.conf /etc/nginx/sites-available/homelab
sudo ln -s /etc/nginx/sites-available/homelab /etc/nginx/sites-enabled/homelab
# Management
sudo nginx -t # Test config sudo nginx -t # Test config
sudo systemctl reload nginx # Reload sudo systemctl reload nginx # Reload (use restart for major changes)
sudo systemctl status nginx # Status
``` ```
### SSL Certificate Management ### Systemd Services
```bash ```bash
sudo certbot --nginx -d ak-homelab.duckdns.org sudo systemctl status nginx copyparty
sudo systemctl enable certbot.timer # Auto-renewal sudo systemctl restart <service>
``` ```
## Development Workflow ## Development Workflow
### Task Tracking 1. Edit files in `config/` (these are the live configs for Docker via symlink)
Use `TODO.md` for centralized task management organized by category (Network & Security, Git & Development, System Configuration, etc.). Mark completed items and update status in documentation. 2. For non-Docker configs: use `/deploy-config` skill or write a `scripts/tmp/` script
3. Update `docs/` if the change affects setup or architecture
4. Update `.claude/skills/homelab-context/SKILL.md` if ports, paths, or rules changed
5. Commit with a clear message
### Configuration Changes ## Documentation Structure
1. Edit files in `config/` directory
2. Test locally when possible
3. Deploy to target locations with sudo commands included in file headers
4. Update documentation status in `docs/services.md`
5. Commit changes with logical separation
### Documentation Structure - `docs/` — Human-readable reference. Keep for migration and troubleshooting.
- `docs/`: Technical documentation for setup procedures - `README.md` — Overview and current status
- `README.md`: Overview and current status - `TODO.md` — Active task tracking
- `TODO.md`: Active task tracking - `.claude/skills/homelab-context/SKILL.md` — AI-optimized operational facts (keep in sync with reality)
- Configuration files self-document deployment locations in headers
### Network Configuration Notes For detailed install steps per service, see `docs/services.md`.
- System uses dual ethernet ports (enp3s0f0, enp4s0) - stick to enp4s0 consistently
- Port conflicts: SSH (2222), Gitea SSH (2223), Gitea Web (3000)
- Router forwarding: HTTP (80), HTTPS (443), SSH (2222), Git SSH (2223)
### Service URLs
- **Local access**: http://192.168.0.100/servicename/
- **External access**: https://ak-homelab.duckdns.org/servicename/
- **Gitea SSH**: ssh://git@ak-homelab.duckdns.org:2223
- **Voice Assistant**: http://127.0.0.1:8880 (local TTS server)
- **Copyparty WebDAV**: https://ak-homelab.duckdns.org/files/ (for X-plore, rclone, etc.)
### Voice Assistant Commands
```bash
# Enable voice mode (starts server and configures voice-mode)
./scripts/enable-voice.sh
# Disable voice mode (stops server)
./scripts/disable-voice.sh
# Manual voice server management
cd voice-server
poetry run voice-server # Start server
poetry install # Install dependencies
poetry run pytest # Run tests
# Test TTS directly
curl -X POST "http://127.0.0.1:8880/v1/audio/speech" \
-H "Content-Type: application/json" \
-d '{"input": "Hello world!", "voice": "ryan"}' \
--output test.wav
```
### Disk Usage Analysis Tools
```bash
# System-level disk space overview (recommended for regular checks)
duf # Modern df with colors and progress bars
# Directory size analysis
dust /home/hoborg # Quick visual summary with tree view
dust /home/hoborg --depth 3 # Limit depth for large directories
# Interactive cleanup and detailed analysis
ncdu /home/hoborg # Navigate with arrow keys, delete with 'd'
ncdu /opt/docker # Analyze Docker data usage
ncdu ~ # Scan home directory for large files
# Usage patterns:
# 1. duf - Quick system health check (like htop for disk space)
# 2. dust - Fast overview of directory sizes (like tree with sizes)
# 3. ncdu - Deep dive cleanup and file management (like htop for files)
```
### WebDAV Client Setup
```bash
# X-plore File Manager (Android)
# Server: ak-homelab.duckdns.org
# Path: /files/
# Port: 443 (HTTPS)
# Username: hoborg
# Password: AdminPass2024!
# rclone configuration
rclone config create homelab-webdav webdav \
url=https://ak-homelab.duckdns.org/files/ \
vendor=other \
user=hoborg \
pass=$(rclone obscure "AdminPass2024!")
# Mount with rclone
rclone mount homelab-webdav: ~/homelab-files --daemon
# Test WebDAV
curl -X PROPFIND https://hoborg:AdminPass2024!@ak-homelab.duckdns.org/files/ \
-H "Depth: 1" -H "Content-Type: text/xml"
```
- Always edit the local configs before when possible, and then copy them to the proper location. Instead of editing system files directly (and then losing the config and it won't be in this repo)
- Never run sudo commands, instead create a script in scripts/tmp and ask the user to run it.
## Security Hardening Guidelines ## Security Hardening Guidelines
- When working on security hardening, make sure you **understand the service needs** first, to ensure the security doesn't interfre with normal operations (e.g. some services need read/write filesystem access, not just read)
- Some containers (like Gitea with s6-overlay) need root start then privilege drop via USER_UID/USER_GID environment variables - Understand service needs before applying restrictions (some need rw filesystem access)
- Test each security change individually, not in batches - Test each security change individually, not in batches
- Network access patterns matter: SSH Git needs direct access, HTTP can be proxied through localhost - Network access patterns matter: SSH Git needs direct access, HTTP can go through localhost proxy
- DO NOT set Docker user: directive for services using s6-overlay init systems (breaks initialization)
## Claude Development Guidelines
### CRITICAL: Anti-Pattern Reminders
1. **RESEARCH FIRST** — Check existing solutions in order: pacman → AUR → GitHub
2. **VERIFY SYNTAX BEFORE WRITING** — Use `--help`, test in parts, never assume argument formats
3. **STICK TO THE CHOSEN SOLUTION** — Don't drift mid-implementation
4. **PREFER SIMPLE OVER COMPLEX** — Existing tools > custom scripts; bash > python for simple tasks
5. **CHECK PACKAGE REPOS FIRST**`pacman -Ss` before any manual installation
### Failure Patterns to Avoid
- ❌ Broken syntax without testing commands first
- ❌ Switching solutions mid-implementation
- ❌ Overcomplicating when simple solutions exist
- ❌ Installing random scripts before checking packages
### Success Pattern
- ✅ Research → Choose → Verify syntax → Implement → Test
- ✅ pacman → AUR → GitHub (preference order)
- ✅ Use proven, maintained tools over custom scripts

View File

@@ -13,6 +13,7 @@ Setting up a personal homelab using a ThinkPad laptop running Arch Linux to move
- [x] Self-hosted media server (Jellyfin) - [x] Self-hosted media server (Jellyfin)
- [x] AI voice assistant (local TTS with Piper) - [x] AI voice assistant (local TTS with Piper)
- [x] Reverse proxy with nginx for multiple services - [x] Reverse proxy with nginx for multiple services
- [x] Geographic IP blocking (geoip-shell whitelist)
- [ ] Gradual migration from commercial cloud services - [ ] Gradual migration from commercial cloud services
## Hardware ## Hardware
@@ -35,6 +36,7 @@ Setting up a personal homelab using a ThinkPad laptop running Arch Linux to move
- ✅ Dotfiles management with yadm configured and merged - ✅ Dotfiles management with yadm configured and merged
- ✅ Development environment setup completed - ✅ Development environment setup completed
- ✅ Network domain setup (DuckDNS + Nginx reverse proxy + SSL) - ✅ Network domain setup (DuckDNS + Nginx reverse proxy + SSL)
- ✅ Geographic IP blocking (geoip-shell whitelist for European countries)
- ✅ Gitea Git server running (Docker container) - ✅ Gitea Git server running (Docker container)
- ✅ Copyparty file server with working WebDAV support - ✅ Copyparty file server with working WebDAV support
- ✅ Jellyfin media server (Docker container) - ✅ Jellyfin media server (Docker container)
@@ -60,6 +62,7 @@ homelab/
├── docs/ # Detailed documentation ├── docs/ # Detailed documentation
│ ├── system-setup.md # Arch Linux installation & config │ ├── system-setup.md # Arch Linux installation & config
│ ├── network-security.md # SSH, DNS, VPN, firewall │ ├── network-security.md # SSH, DNS, VPN, firewall
│ ├── geoip-blocking.md # Geographic IP blocking setup
│ ├── services.md # Self-hosted services │ ├── services.md # Self-hosted services
│ ├── voice-assistant.md # AI voice setup with Piper TTS │ ├── voice-assistant.md # AI voice setup with Piper TTS
│ └── troubleshooting/ # Solutions & troubleshooting guides │ └── troubleshooting/ # Solutions & troubleshooting guides
@@ -83,6 +86,7 @@ homelab/
### Documentation Files ### Documentation Files
- **[docs/system-setup.md](docs/system-setup.md)** - Complete Arch Linux installation, TTY config, desktop setup - **[docs/system-setup.md](docs/system-setup.md)** - Complete Arch Linux installation, TTY config, desktop setup
- **[docs/network-security.md](docs/network-security.md)** - SSH hardening, DuckDNS, WireGuard VPN, firewall setup - **[docs/network-security.md](docs/network-security.md)** - SSH hardening, DuckDNS, WireGuard VPN, firewall setup
- **[docs/geoip-blocking.md](docs/geoip-blocking.md)** - Geographic IP blocking with geoip-shell whitelist
- **[docs/services.md](docs/services.md)** - Self-hosted services: Git hosting, cloud storage, media server - **[docs/services.md](docs/services.md)** - Self-hosted services: Git hosting, cloud storage, media server
- **[docs/voice-assistant.md](docs/voice-assistant.md)** - AI voice assistant setup with Piper TTS and FastAPI - **[docs/voice-assistant.md](docs/voice-assistant.md)** - AI voice assistant setup with Piper TTS and FastAPI
- **[TODO.md](TODO.md)** - Centralized task list with progress tracking by category - **[TODO.md](TODO.md)** - Centralized task list with progress tracking by category
@@ -93,5 +97,5 @@ homelab/
- **System**: Arch Linux with XFCE desktop, ter-124b TTY font, Colemak layout - **System**: Arch Linux with XFCE desktop, ter-124b TTY font, Colemak layout
- **Network**: Static IP (192.168.0.100), SSH port 2222, DuckDNS (ak-homelab.duckdns.org) - **Network**: Static IP (192.168.0.100), SSH port 2222, DuckDNS (ak-homelab.duckdns.org)
- **Services**: Nginx reverse proxy, Gitea Git server, Copyparty file server with WebDAV, Jellyfin media server, AI voice assistant - **Services**: Nginx reverse proxy, Gitea Git server, Copyparty file server with WebDAV, Jellyfin media server, AI voice assistant
- **Security**: SSH hardened, SSL certificates active, WebDAV authentication enabled - **Security**: SSH hardened, SSL certificates active, WebDAV authentication enabled, geographic IP blocking (geoip-shell whitelist)
- **Development**: yadm dotfiles, tmux with temperature monitoring, zsh with proper history - **Development**: yadm dotfiles, tmux with temperature monitoring, zsh with proper history

43
TODO.md
View File

@@ -1,13 +1,21 @@
# Homelab TODO List # Homelab TODO List
## HIGH PRIO
- [ ] Move all bigger storages to NAS if it isn't there already
- Gitea
- Nextcloud
- [ ] Unify YADM configs across different systems
- use YADM alternatives method if needed for conflicts
## Network & Security ## Network & Security
- [x] DuckDNS dynamic DNS setup *(completed - ak-homelab.duckdns.org)* - [x] DuckDNS dynamic DNS setup *(completed - ak-homelab.duckdns.org)*
- [x] SSH security hardening *(documented in network-security.md)* - [x] SSH security hardening *(documented in network-security.md)*
- [x] Figure out why laptop IP changes: Different eth ports have different MAC? - [x] Figure out why laptop IP changes: Different eth ports have different MAC?
- [x] Router port forwarding configuration - [x] Router port forwarding configuration
- [ ] !!! Set up geoblocking for SSH. Rest of SSH hardening already done. - [x] !!! Set up geoblocking for SSH. Rest of SSH hardening already done.
- [ ] !!! Modify syncthing to sync the NAS folders where appropriate (e.g. Logseq) - [ ] !!! Modify syncthing to sync the NAS folders where appropriate (e.g. Logseq)
- [ ] Dockerize everything and use symlinks for dockerfiles (tired of constantly copying stuff over) - [x] Dockerize everything and use symlinks for dockerfiles — all docker-compose files symlinked from `/opt/docker/` to repo `config/docker/`
- [ ] !!! IMPORTANT: Run setup scripts made by security reviewer agent - [ ] !!! IMPORTANT: Run setup scripts made by security reviewer agent
- [ ] Ran out of AI quota mid-security review so continue where we left off. Some scripts created but it's not - [ ] Ran out of AI quota mid-security review so continue where we left off. Some scripts created but it's not
complete yet complete yet
@@ -23,7 +31,7 @@
- [ ] WireGuard VPN server configuration - [ ] WireGuard VPN server configuration
- [ ] UFW firewall setup and rules - [ ] UFW firewall setup and rules
- [ ] fail2ban for intrusion prevention - [ ] fail2ban for intrusion prevention
- [ ] Security enhancement for VNC connections (in the meantime: only run the vnc service for short time while we are using it) - [x] VNC setup for remote desktop access *(TigerVNC installed, enabled temporarily via SSH when needed)*
## Git & Development ## Git & Development
- [x] Gitea Docker container setup *(completed - running on port 3000)* - [x] Gitea Docker container setup *(completed - running on port 3000)*
@@ -91,9 +99,8 @@ Lower priority - mostly using SSH or TTY anyways
- [x] Shared media folders with Copyparty (Music, Videos, shared) - [x] Shared media folders with Copyparty (Music, Videos, shared)
- [ ] Set up self-hosted chat server (Matrix or Mattermost) - [ ] Set up self-hosted chat server (Matrix or Mattermost)
- [ ] Install monitoring and management tools *(in progress)* - [ ] Install monitoring and management tools *(in progress)*
- [ ] Portainer (Docker management with built-in auth) - [x] Portainer (Docker management with built-in auth)
- [ ] Glances (system monitoring with nginx basic auth) - [x] Glances (system monitoring with nginx basic auth)
- [ ] Cockpit (system administration with PAM auth)
- [ ] lazydocker (terminal Docker management) - [ ] lazydocker (terminal Docker management)
- [ ] Configure nginx basic auth for Glances endpoint - [ ] Configure nginx basic auth for Glances endpoint
- [ ] Update nginx reverse proxy config for new admin services - [ ] Update nginx reverse proxy config for new admin services
@@ -101,14 +108,16 @@ Lower priority - mostly using SSH or TTY anyways
- [ ] Set up Nextcloud for advanced file synchronization features - [ ] Set up Nextcloud for advanced file synchronization features
- Copyparty covers basic file sharing needs - Copyparty covers basic file sharing needs
- [x] Set up reverse proxy with SSL certificates *(completed - HTTPS working with auto-renewal)* - [x] Set up reverse proxy with SSL certificates *(completed - HTTPS working with auto-renewal)*
- [ ] Make sure all services are dockerized unless we have a good reason not to - [x] Make sure all services are dockerized unless we have a good reason not to
- Gitea: ✅ Docker - Gitea: ✅ Docker
- Jellyfin: ✅ Docker - Jellyfin: ✅ Docker
- Copyparty: ❌ systemd service (consider dockerizing) - qBittorrent: ✅ Docker
- Nginx: ❌ system package (fine as-is for reverse proxy)
- Portainer: ✅ Docker - Portainer: ✅ Docker
- Glances: ❌ system package (web server mode) - Copyparty: ✅ Docker
- Cockpit: ❌ system package (system integration required) - Glances: ❌ systemd (needs full system access for accurate monitoring)
- Netdata: ❌ systemd (needs full system access for accurate monitoring)
- Syncthing: ❌ systemd (Docker permission issues with config directory)
- Nginx: ❌ system package (reverse proxy, system integration required)
## Hardware & Troubleshooting ## Hardware & Troubleshooting
- [ ] Fix bluetooth audio connectivity issues - [ ] Fix bluetooth audio connectivity issues
@@ -116,6 +125,18 @@ Lower priority - mostly using SSH or TTY anyways
- [x] Figure out drag and drop window tiling solution -> workaround with keyboard shortcuts - [x] Figure out drag and drop window tiling solution -> workaround with keyboard shortcuts
- [ ] Install multimedia codecs and applications - [ ] Install multimedia codecs and applications
## Docker Storage Migration
- [x] Move Gitea storage to NAS — migrated to `/mnt/nas/docker-data/gitea/data` (USER_UID=1024:100 to match NAS ownership)
- [x] Move Nextcloud storage to NAS — migrated to `/mnt/nas/docker-data/nextcloud/`
- [ ] Persist copyparty index database to NAS — currently rebuilt from scratch on every restart (slow, scans entire NAS); mount `/mnt/nas/.copyparty-db` into the container so the index survives restarts
- [ ] Clean up old leftover data: `/opt/docker/gitea/data/`, `/opt/docker/gitea/data.old/`, Docker volumes `gitea_gitea`, `nextcloud_nextcloud_data`, `nextcloud_nextcloud_db`
## Docker Image Upgrades
See `docs/docker-upgrade-plan.md` for full plan. Key warnings:
- **Jellyfin**: v10.11+ does a major EF Core DB migration (`library.db``jellyfin.db`). Known to hang at "Saving BaseItem entries". **Back up jellyfin data volume before upgrading.**
- **Redis**: Stay on `7-alpine` — Redis 8 has ACL breaking changes, not worth upgrading for a cache role
- All containers are behind on updates as of 2026-02-27 (see plan doc for details)
## Security & Maintenance ## Security & Maintenance
- [ ] Configure automatic security updates - [ ] Configure automatic security updates
- [ ] Set up system monitoring and alerting - [ ] Set up system monitoring and alerting

View File

@@ -1,71 +0,0 @@
# Copyparty Configuration for Homelab
# DEPLOYMENT LOCATION: /home/hoborg/.config/copyparty/copyparty.conf
[global]
# Network settings
i: 127.0.0.1
p: 8082
rp-loc: /files
# Reverse proxy configuration
rproxy: -1
# Security and features
usernames
chpw
chpw-db: /home/hoborg/.config/copyparty/passwords.json
e2dsa
dedup
dotpart
# Upload settings
u2ts: c
chmod-f: 644
chmod-d: 755
# Server name
name: homelab-files
[accounts]
guest: SecurePass2024!
hoborg: AdminPass2024!
[/shared]
/mnt/nas/shared
accs:
rw: guest
rwmd: hoborg
[/documents]
/mnt/nas/documents
accs:
rwmd: hoborg
[/music]
/mnt/nas/music
accs:
rw: guest
rwmd: hoborg
[/videos]
/mnt/nas/videos
accs:
rw: guest
rwmd: hoborg
[/private]
/mnt/nas/private
accs:
rwmd: hoborg
[/pictures]
/mnt/nas/pictures
accs:
rw: guest
rwmd: hoborg
[/torrent]
/mnt/nas/torrent
accs:
rw: guest
rwmd: hoborg

View File

@@ -0,0 +1,90 @@
# Copyparty Docker Compose Configuration
# Deploy with: sudo mkdir -p /opt/docker/copyparty && sudo cp config/docker/copyparty/docker-compose.yml /opt/docker/copyparty/
# Start with: cd /opt/docker/copyparty && sudo docker-compose up -d
# COPYPARTY CONFIGURATION
# - File server with upload/download capabilities
# - WebDAV support for mobile and desktop clients
# - Bound to localhost only (reverse proxy required)
# - Configuration stored in repo at config/copyparty/copyparty.conf
services:
copyparty:
image: copyparty/ac:latest
container_name: copyparty
restart: unless-stopped
# Environment
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Budapest
# Network - bind to localhost for security
ports:
- "127.0.0.1:8082:3923" # Web UI (reverse proxy only)
# Volume mounts
volumes:
# Password database
- /home/hoborg/.config/copyparty/passwords.json:/home/hoborg/.config/copyparty/passwords.json
# NAS storage volumes (mapped to container paths)
- /mnt/nas/shared:/w/shared:rw
- /mnt/nas/documents:/w/documents:rw
- /mnt/nas/music:/w/music:rw
- /mnt/nas/videos:/w/videos:rw
- /mnt/nas/private:/w/private:rw
- /mnt/nas/pictures:/w/pictures:rw
- /mnt/nas/installers:/w/installers:rw
- /mnt/nas/torrent:/w/torrent:rw
# Command with inline volume configuration using -v flag
command:
- --rp-loc=/files
- --name=homelab-files
- --usernames
- --chpw
- --chpw-db=/home/hoborg/.config/copyparty/passwords.json
- --xff-src=172.0.0.0/8
- --rproxy=-1
- -e2dsa
- --dedup
- --dotpart
- -a
- hoborg:AdminPass2024!
- -a
- guest:SecurePass2024!
- -v
- /w/shared:shared:r:rw,guest:rwmd,hoborg
- -v
- /w/documents:documents:rwmd,hoborg
- -v
- /w/music:music:r:rw,guest:rwmd,hoborg
- -v
- /w/videos:videos:r:rw,guest:rwmd,hoborg
- -v
- /w/private:private:rwmd,hoborg
- -v
- /w/pictures:pictures:r:rw,guest:rwmd,hoborg
- -v
- /w/installers:installers:r:rw,guest:rwmd,hoborg
- -v
- /w/torrent:torrent:r:rw,guest:rwmd,hoborg
# Resource limits
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
# Health check
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3923/files/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s

View File

@@ -1,13 +1,3 @@
# Docker Daemon Configuration (Minimal Working Version)
# Deploy to: /etc/docker/daemon.json
#
# Setup commands:
# sudo cp config/docker/daemon.json /etc/docker/
# sudo systemctl restart docker
#
# Note: Removed problematic security settings that caused read-only filesystem issues.
# Only keeping essential logging configuration.
{ {
"log-driver": "json-file", "log-driver": "json-file",
"log-opts": { "log-opts": {

View File

@@ -14,11 +14,11 @@ networks:
services: services:
server: server:
image: gitea/gitea:latest image: gitea/gitea:1.24
container_name: gitea container_name: gitea
environment: environment:
- USER_UID=1000 - USER_UID=1024
- USER_GID=1000 - USER_GID=100
- GITEA__server__ROOT_URL=https://ak-homelab.duckdns.org/gitea/ - GITEA__server__ROOT_URL=https://ak-homelab.duckdns.org/gitea/
- GITEA__server__SSH_PORT=2223 - GITEA__server__SSH_PORT=2223
restart: unless-stopped restart: unless-stopped
@@ -29,7 +29,7 @@ services:
networks: networks:
- gitea - gitea
volumes: volumes:
- ./data:/data - /mnt/nas/docker-data/gitea/data:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro

View File

@@ -11,7 +11,7 @@
services: services:
jellyfin: jellyfin:
image: jellyfin/jellyfin:latest image: jellyfin/jellyfin:10.11.6
container_name: jellyfin container_name: jellyfin
restart: unless-stopped restart: unless-stopped

View File

@@ -10,13 +10,18 @@ services:
image: mariadb:10.11 image: mariadb:10.11
restart: unless-stopped restart: unless-stopped
container_name: nextcloud-db container_name: nextcloud-db
user: "1024:100" # Match NAS file ownership (uid 1024, gid 100) to bypass chown on NFS
environment: environment:
- MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password
- MYSQL_PASSWORD_FILE=/run/secrets/mysql_password - MYSQL_PASSWORD_FILE=/run/secrets/mysql_password
- MYSQL_DATABASE=nextcloud - MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud - MYSQL_USER=nextcloud
- MARIADB_INITDB_SKIP_TZINFO=1 # Skip timezone loading on NFS
volumes: volumes:
- nextcloud_db:/var/lib/mysql - /mnt/nas/docker-data/nextcloud/db-data:/var/lib/mysql
command:
- --innodb_use_native_aio=0 # Required for NFS
- --innodb_flush_method=O_DIRECT # Better NFS performance
networks: networks:
- nextcloud - nextcloud
secrets: secrets:
@@ -31,7 +36,7 @@ services:
- nextcloud - nextcloud
nextcloud-app: nextcloud-app:
image: nextcloud:29 image: nextcloud:32
restart: unless-stopped restart: unless-stopped
container_name: nextcloud-app container_name: nextcloud-app
ports: ports:
@@ -49,7 +54,7 @@ services:
- OVERWRITEHOST=ak-homelab.duckdns.org - OVERWRITEHOST=ak-homelab.duckdns.org
- OVERWRITEWEBROOT=/cloud - OVERWRITEWEBROOT=/cloud
volumes: volumes:
- nextcloud_data:/var/www/html - /mnt/nas/docker-data/nextcloud/app-data:/var/www/html
# External storage mounts for existing folders # External storage mounts for existing folders
- /home/hoborg/shared:/external_storage/shared:rw - /home/hoborg/shared:/external_storage/shared:rw
- /home/hoborg/Documents:/external_storage/documents:rw - /home/hoborg/Documents:/external_storage/documents:rw
@@ -74,10 +79,6 @@ secrets:
nextcloud_admin_password: nextcloud_admin_password:
file: /home/hoborg/creds/nextcloud_admin_password.txt file: /home/hoborg/creds/nextcloud_admin_password.txt
volumes:
nextcloud_db:
nextcloud_data:
networks: networks:
nextcloud: nextcloud:
driver: bridge driver: bridge

View File

@@ -0,0 +1,44 @@
# Portainer Docker Compose Configuration
# Deploy with: sudo mkdir -p /opt/docker/portainer && sudo cp config/docker/portainer/docker-compose.yml /opt/docker/portainer/
# Start with: cd /opt/docker/portainer && sudo docker-compose up -d
# PORTAINER CONFIGURATION
# - Web UI for Docker container management
# - Bound to localhost only (reverse proxy recommended)
# - Persistent data storage in named volume
services:
portainer:
image: portainer/portainer-ce:2.39.0
container_name: portainer
restart: unless-stopped
# Environment
environment:
- TZ=Europe/Budapest
# Network - bind to localhost for security
ports:
- "127.0.0.1:9000:9000" # Web UI (reverse proxy only)
# Volume mounts
volumes:
# Docker socket for container management
- /var/run/docker.sock:/var/run/docker.sock
# Portainer data
- portainer_data:/data
# Resource limits
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.1'
memory: 128M
# No healthcheck — Portainer is distroless (no shell available)
volumes:
portainer_data:

View File

@@ -11,7 +11,7 @@
services: services:
qbittorrent: qbittorrent:
image: linuxserver/qbittorrent:latest image: linuxserver/qbittorrent:5.1.4
container_name: qbittorrent container_name: qbittorrent
restart: unless-stopped restart: unless-stopped
@@ -23,7 +23,7 @@ services:
- WEBUI_PORT=8080 - WEBUI_PORT=8080
# Security hardening # Security hardening
user: "1000:1000" # NOTE: No user: directive — linuxserver uses s6-overlay (starts as root, drops to PUID/PGID)
read_only: false # qBittorrent needs write access for downloads read_only: false # qBittorrent needs write access for downloads
tmpfs: tmpfs:
- /tmp:noexec,nosuid,size=100m - /tmp:noexec,nosuid,size=100m

View File

@@ -5,9 +5,16 @@ server {
listen 80 default_server; listen 80 default_server;
server_name ak-homelab.duckdns.org _; server_name ak-homelab.duckdns.org _;
# Redirect HTTP to HTTPS # Allow Let's Encrypt ACME challenges
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect all other HTTP to HTTPS
location / {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
}
server { server {
listen 443 ssl default_server; listen 443 ssl default_server;
@@ -176,6 +183,32 @@ server {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
} }
# Nextcloud cloud storage
location /cloud/ {
proxy_pass http://127.0.0.1:8083/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Nextcloud specific headers
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
# Large file upload support
client_max_body_size 10G;
client_body_buffer_size 128k;
# Upload timeout settings
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# Buffering settings for uploads
proxy_buffering off;
proxy_request_buffering off;
}
ssl_certificate /etc/letsencrypt/live/ak-homelab.duckdns.org/fullchain.pem; # managed by Certbot ssl_certificate /etc/letsencrypt/live/ak-homelab.duckdns.org/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/ak-homelab.duckdns.org/privkey.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/ak-homelab.duckdns.org/privkey.pem; # managed by Certbot

View File

@@ -1,12 +1,3 @@
# SSH Honeypot Service
# Deploy to: /etc/systemd/system/ssh-honeypot.service
#
# Setup commands:
# sudo cp config/systemd/ssh-honeypot.service /etc/systemd/system/
# sudo systemctl daemon-reload
# sudo systemctl enable ssh-honeypot.service
# sudo systemctl start ssh-honeypot.service
[Unit] [Unit]
Description=SSH Honeypot (Port 22) Description=SSH Honeypot (Port 22)
After=network.target After=network.target

View File

@@ -140,10 +140,10 @@
<h3>Copyparty</h3> <h3>Copyparty</h3>
<p>File Server & WebDAV</p> <p>File Server & WebDAV</p>
</a> </a>
<a href="#" class="service disabled" onclick="return false;"> <a href="/cloud/" class="service">
<i class="fas fa-cloud"></i> <i class="fas fa-cloud"></i>
<h3>Nextcloud</h3> <h3>Nextcloud</h3>
<p>Temporarily Disabled</p> <p>Cloud Storage & Collaboration</p>
</a> </a>
<a href="/media/" class="service"> <a href="/media/" class="service">
<i class="fas fa-play-circle"></i> <i class="fas fa-play-circle"></i>

181
docs/geoip-blocking.md Normal file
View File

@@ -0,0 +1,181 @@
# GeoIP Blocking with geoip-shell
## Overview
**Tool:** geoip-shell v0.7.5
**Repository:** https://github.com/friendly-bits/geoip-shell
**Installation Method:** Manual from GitHub repository
**Purpose:** Geographic IP blocking for enhanced security
## Installation
```bash
# Clone the repository
git clone https://github.com/friendly-bits/geoip-shell.git
cd geoip-shell
# Install the tool
sudo make install
```
## Current Configuration
### Core Settings
- **Firewall Backend:** iptables
- **IP Lists Source:** RIPE (Réseaux IP Européens)
- **Network Interfaces:** All interfaces protected
- **LAN Detection:** Automatic subnet detection enabled
### Update Mechanism
- **Cron Service:** ✅ Enabled
- **Update Schedule:** Daily at 4:08 AM (`8 4 * * *`)
- **Last Update:** September 17, 2025 at 00:57:41
- **Persistence:** ✅ Enabled (survives reboots)
- **Backup:** ✅ Automatic IP lists backup enabled
### Persistence Cron Job
- **Schedule:** On reboot (`@reboot`)
- **Command:** `/usr/bin/geoip-shell-run.sh restore -a`
- **Purpose:** Restores geoip-shell firewall rules after system reboot
## Inbound Geoblocking Configuration
### Blocking Mode
- **Type:** Whitelist (only specified countries allowed)
- **IP Families:** IPv4 and IPv6 supported
### Whitelisted Countries
```
AL (Albania), AD (Andorra), AM (Armenia), AT (Austria), AZ (Azerbaijan)
BY (Belarus), BE (Belgium), BA (Bosnia and Herzegovina), BG (Bulgaria)
HR (Croatia), CY (Cyprus), CZ (Czech Republic), DK (Denmark)
EE (Estonia), FO (Faroe Islands), FI (Finland), FR (France)
GE (Georgia), DE (Germany), GI (Gibraltar), GR (Greece)
GG (Guernsey), HU (Hungary), IS (Iceland), IE (Ireland)
IM (Isle of Man), IT (Italy), JE (Jersey), KZ (Kazakhstan)
LV (Latvia), LI (Liechtenstein), LT (Lithuania), LU (Luxembourg)
MT (Malta), MD (Moldova), MC (Monaco), ME (Montenegro)
NL (Netherlands), MK (North Macedonia), NO (Norway), PL (Poland)
PT (Portugal), RO (Romania), RU (Russia), SM (San Marino)
RS (Serbia), SK (Slovakia), SI (Slovenia), ES (Spain)
SE (Sweden), CH (Switzerland), TR (Turkey), UA (Ukraine)
GB (United Kingdom), VA (Vatican City)
```
### Network Exceptions (Always Allowed)
**IPv4 Networks:**
- `172.18.0.0/16` - Docker network
- `172.17.0.0/16` - Docker network
- `169.254.0.0/16` - Link-local addresses
- `192.168.0.0/24` - Local LAN
- `172.20.0.0/16` - Docker network
- `172.19.0.0/16` - Docker network
**IPv6 Networks:**
- `fdaa:bbcc:ddee::/64` - Custom network
- `fe80::/10` - Link-local addresses
### Protocol Coverage
- **TCP:** All destination ports blocked for non-whitelisted countries
- **UDP:** All destination ports blocked for non-whitelisted countries
### Firewall Status
- **IPv4 Chain:** ✅ Enabled and active
- **IPv6 Chain:** ✅ Enabled and active
- **Whitelist Rules:** ✅ Properly configured
## Outbound Geoblocking
- **Status:** Disabled (no outbound restrictions)
## System Health
- **Overall Status:** ✅ No problems detected
- **Firewall Integration:** ✅ Working correctly
- **Update Process:** ✅ Functioning properly
## Security Impact
### Protection Provided
1. **Geographic Blocking:** Blocks all traffic from countries not in whitelist
2. **Comprehensive Coverage:** Both IPv4 and IPv6 protection
3. **Full Protocol Coverage:** TCP and UDP traffic controlled
4. **Network Awareness:** Automatically detects and allows local networks
5. **Persistence:** Rules survive system reboots
6. **Automatic Updates:** IP lists updated daily
### Integration with Existing Security
- **Complements fail2ban:** Provides geographic layer above intrusion detection
- **Works with UFW:** Uses iptables backend compatible with UFW
- **Docker Compatible:** Automatically allows Docker networks
- **LAN Friendly:** Preserves local network access
## Monitoring and Maintenance
### Status Checking
```bash
geoip-shell status
```
### Log Locations
- Main logs: `/var/log/geoip-shell.log`
- System logs: `journalctl -u geoip-shell`
### Update Verification
- Automatic daily updates at 4:18 AM
- Last update timestamp visible in status
- Backup of IP lists maintained
## Configuration Philosophy
This setup follows a **whitelist approach** focusing on:
- **Trusted Regions:** European countries plus select others
- **Local Access:** Full LAN and Docker network access preserved
- **Minimal Disruption:** Automatic detection of local networks
- **Comprehensive Protection:** Both inbound directions covered
## Manual Setup Process
The tool was installed manually with interactive prompts rather than scripted installation due to security considerations. Key decisions made during setup:
1. **Whitelist Mode:** Chosen over blacklist for better control
2. **European Focus:** Primary whitelist consists of European countries
3. **Network Exceptions:** Docker and LAN networks automatically detected
4. **Dual Stack:** Both IPv4 and IPv6 protection enabled
5. **Full Protocol Coverage:** TCP and UDP both protected
## Troubleshooting
### Common Issues
- **Service Not Starting:** Check `systemctl status geoip-shell`
- **Rules Not Applied:** Verify iptables chains with `iptables -L`
- **Update Failures:** Check `/var/log/geoip-shell.log`
### Recovery Commands
```bash
# Restart service
sudo systemctl restart geoip-shell
# Reload configuration
sudo geoip-shell reload
# Check for issues
sudo geoip-shell status
```
## Future Considerations
### Potential Enhancements
- **Custom Country Lists:** Add/remove countries based on threat intelligence
- **Time-based Rules:** Different rules for different times of day
- **Integration with Monitoring:** Alert on blocked country attempts
- **Backup Configurations:** Document alternative configurations
### Monitoring Improvements
- **Log Analysis:** Regular review of blocked attempts by country
- **Performance Impact:** Monitor system resource usage
- **False Positives:** Track legitimate traffic being blocked
## References
- **Official Repository:** https://github.com/friendly-bits/geoip-shell
- **Documentation:** Available in repository wiki
- **Issue Tracking:** GitHub issues for bug reports and feature requests

View File

@@ -366,6 +366,245 @@ sudo fail2ban-client status sshd
sudo fail2ban-client get sshd banned sudo fail2ban-client get sshd banned
``` ```
## Geographic IP Blocking with geoip-shell
### Overview
**Status:** ✅ **Implemented** - Whitelist mode protecting all interfaces
**Tool:** geoip-shell v0.7.5
**Repository:** https://github.com/friendly-bits/geoip-shell
**Purpose:** Geographic IP blocking for enhanced security
### Current Configuration
- **Firewall Backend:** iptables
- **IP Lists Source:** RIPE (Réseaux IP Européens)
- **Network Interfaces:** All interfaces protected
- **LAN Detection:** Automatic subnet detection enabled
- **Mode:** Whitelist (only specified countries allowed)
- **IP Families:** IPv4 and IPv6 supported
- **Update Schedule:** Daily at 4:18 AM
- **Last Update:** September 17, 2025 at 00:57:41
### Whitelisted Countries
**Configuration:** All EU member states plus European territories (full list accepted by geoip-shell)
```
AL (Albania), AD (Andorra), AM (Armenia), AT (Austria), AX (Åland Islands), AZ (Azerbaijan)
BY (Belarus), BE (Belgium), BA (Bosnia and Herzegovina), BG (Bulgaria)
HR (Croatia), CY (Cyprus), CZ (Czech Republic), DK (Denmark)
EE (Estonia), FO (Faroe Islands), FI (Finland), FR (France)
GE (Georgia), DE (Germany), GI (Gibraltar), GR (Greece)
GG (Guernsey), HU (Hungary), IS (Iceland), IE (Ireland)
IM (Isle of Man), IT (Italy), JE (Jersey), KZ (Kazakhstan)
LV (Latvia), LI (Liechtenstein), LT (Lithuania), LU (Luxembourg)
MT (Malta), MD (Moldova), MC (Monaco), ME (Montenegro)
NL (Netherlands), MK (North Macedonia), NO (Norway), PL (Poland)
PT (Portugal), RO (Romania), RU (Russia), SM (San Marino)
RS (Serbia), SK (Slovakia), SI (Slovenia), ES (Spain)
SE (Sweden), CH (Switzerland), TR (Turkey), UA (Ukraine)
GB (United Kingdom), VA (Vatican City)
```
### Network Exceptions (Always Allowed)
**IPv4 Networks:**
- `172.18.0.0/16` - Docker network
- `172.17.0.0/16` - Docker network
- `169.254.0.0/16` - Link-local addresses
- `192.168.0.0/24` - Local LAN
- `172.20.0.0/16` - Docker network
- `172.19.0.0/16` - Docker network
**IPv6 Networks:**
- `fdaa:bbcc:ddee::/64` - Custom network
- `fe80::/10` - Link-local addresses
### Security Impact
1. **Geographic Blocking:** Blocks all traffic from countries not in whitelist
2. **Comprehensive Coverage:** Both IPv4 and IPv6 protection
3. **Full Protocol Coverage:** TCP and UDP traffic controlled
4. **Network Awareness:** Automatically detects and allows local networks
5. **Persistence:** Rules survive system reboots
6. **Automatic Updates:** IP lists updated daily
### Integration with Existing Security
- **Complements fail2ban:** Provides geographic layer above intrusion detection
- **Works with UFW:** Uses iptables backend compatible with UFW
- **Docker Compatible:** Automatically allows Docker networks
- **LAN Friendly:** Preserves local network access
### Monitoring
```bash
# Check geoip-shell status
geoip-shell status
# View logs
journalctl -u geoip-shell
tail -f /var/log/geoip-shell.log
```
### Installation and Setup
**Installation Method:** Interactive CLI setup using geoip-shell installer
The tool was configured using its interactive setup wizard, which prompted for configuration choices and automatically applied all necessary changes to the firewall. This approach ensures proper integration with the existing firewall backend.
**Key Configuration Decisions:**
1. **Whitelist Mode:** Chosen over blacklist for better control
2. **European Focus:** All EU member states plus European territories
3. **Country List:** Complete list of countries accepted by geoip-shell
4. **Network Exceptions:** Docker and LAN networks automatically detected
5. **Dual Stack:** Both IPv4 and IPv6 protection enabled
6. **Full Protocol Coverage:** TCP and UDP both protected
**Setup Command:**
```bash
# Download and run installer
curl -fsSL https://raw.githubusercontent.com/friendly-bits/geoip-shell/main/install.sh | bash
# Run configuration wizard (prompted automatically after install)
geoip-shell configure
```
During the interactive setup, the wizard prompted for:
- Firewall backend selection (iptables chosen)
- IP list source (RIPE selected)
- Mode selection (whitelist chosen)
- Country selection (all EU countries entered)
- Network interface configuration (all interfaces selected)
### Detailed Documentation
For complete setup details, see **[docs/geoip-blocking.md](docs/geoip-blocking.md)**
## Permanent Ban System for Repeat Offenders
### Overview
**Status:** ✅ **Implemented** - Automated permanent banning of persistent attackers
**Script:** `scripts/permanent-ban-repeat-offenders.sh`
**Purpose:** Automatically identify and permanently ban IPs that have been banned by fail2ban more than a threshold number of times
### How It Works
#### Detection Logic
1. **Log Analysis:** Scans `/var/log/fail2ban.log*` for ban entries
2. **IP Extraction:** Extracts IP addresses from ban log entries
3. **Frequency Counting:** Counts how many times each IP has been banned
4. **Threshold Check:** Identifies IPs banned more than the threshold (4 times)
#### Permanent Banning Process
For each repeat offender:
1. **Country Lookup:** Uses `whois` to determine the country of origin
2. **Banlist Update:** Adds IP to `/etc/fail2ban/permanent-banlist.conf`
3. **Firewall Rule:** Creates permanent iptables DROP rule
4. **Persistence:** Saves iptables rules to `/etc/iptables/iptables.rules`
5. **Service Reload:** Reloads fail2ban to recognize the updated banlist
### Configuration
#### Threshold Settings
```bash
THRESHOLD=4 # Ban after 4 fail2ban bans
```
#### File Locations
- **Log File:** `/var/log/permanent-ban.log`
- **Banlist:** `/etc/fail2ban/permanent-banlist.conf`
- **Iptables Rules:** `/etc/iptables/iptables.rules`
#### Cron Schedule
- **Frequency:** Daily at 2:00 AM (`0 2 * * *`)
- **User:** root
- **Command:** `/home/hoborg/homelab/scripts/permanent-ban-repeat-offenders.sh`
### Security Benefits
#### Multi-Layer Protection
1. **fail2ban:** Temporary bans for suspicious activity
2. **Permanent Bans:** Long-term blocking of persistent attackers
3. **Geographic Blocking:** Country-level filtering via geoip-shell
4. **Network-Level:** iptables rules at the firewall level
#### Attack Prevention
- **Brute Force:** Blocks IPs that repeatedly attempt attacks
- **Botnets:** Prevents automated attacks from compromised hosts
- **Persistence:** Maintains bans across system reboots
- **Resource Protection:** Reduces server load from repeat offenders
### Monitoring and Maintenance
#### Log Analysis
```bash
# View permanent ban activity
tail -f /var/log/permanent-ban.log
# Check current permanent bans
cat /etc/fail2ban/permanent-banlist.conf
# View iptables permanent rules
iptables -L | grep DROP
```
#### Manual Execution
```bash
# Run the script manually (requires root)
sudo /home/hoborg/homelab/scripts/permanent-ban-repeat-offenders.sh
```
#### Unban Procedure
To manually unban a permanently banned IP:
```bash
# Remove from banlist
sudo sed -i "/^192\.168\.1\.100/d" /etc/fail2ban/permanent-banlist.conf
# Remove iptables rule (find the rule number first)
sudo iptables -L --line-numbers | grep "192.168.1.100"
sudo iptables -D INPUT <rule_number>
# Save iptables rules
sudo iptables-save > /etc/iptables/iptables.rules
# Reload fail2ban
sudo systemctl reload fail2ban
```
### Integration with Security Stack
#### Complementary Tools
- **fail2ban:** Provides temporary bans that feed into permanent ban detection
- **geoip-shell:** Geographic blocking at the network level
- **UFW:** Additional firewall layer
- **SSH Hardening:** Reduces initial attack surface
#### Workflow
```
Attack Attempt → fail2ban Detection → Temporary Ban → Repeat Offense → Permanent Ban → Geographic Block
```
### Troubleshooting
#### Common Issues
- **Script Not Running:** Check cron job configuration
- **Permission Errors:** Ensure script is executable and paths are correct
- **whois Failures:** Some IPs may not return country information
- **iptables-save Issues:** Check if iptables-persistent is installed
#### Diagnostic Commands
```bash
# Check cron service
sudo systemctl status cron
# Test script manually
sudo bash -x /home/hoborg/homelab/scripts/permanent-ban-repeat-offenders.sh
# Verify iptables rules
sudo iptables -L -n | grep DROP
# Check fail2ban integration
sudo fail2ban-client status
```
## Router Configuration ## Router Configuration
### Port Forwarding ### Port Forwarding

View File

@@ -45,11 +45,19 @@ This document catalogs all security-related configuration files in the homelab r
### Gitea Security ### Gitea Security
- **Config File**: `config/docker/gitea/docker-compose.yml` - **Config File**: `config/docker/gitea/docker-compose.yml`
- **Deploy To**: `/opt/docker/gitea/docker-compose.yml`
- **Security Features**: - **Security Features**:
- Custom SSH port (2223) - Custom SSH port (2223) for Git operations
- Database isolation - Web UI bound to localhost only (127.0.0.1:3000) for reverse proxy
- Volume permissions - Non-root user operation via USER_UID/USER_GID environment variables
- Network restrictions - Resource limits (1G memory)
- Health checking for service monitoring
- s6-overlay compatible configuration (no read-only filesystem)
- **Security Notes**:
- SSH port 2223 bound to all interfaces for external Git access
- Web port 3000 bound to localhost only for nginx proxy security
- Avoid Docker `user:` directive (breaks s6-overlay init system)
- Use environment variables for privilege dropping instead
### Jellyfin Security ### Jellyfin Security
- **Config File**: `config/docker/jellyfin/docker-compose.yml` - **Config File**: `config/docker/jellyfin/docker-compose.yml`

File diff suppressed because it is too large Load Diff

233
docs/system-backups.md Normal file
View File

@@ -0,0 +1,233 @@
# System Backups
Complete backup strategy for the homelab using Restic for encrypted, deduplicated backups to NAS.
## Overview
**Backup Tool**: Restic (modern, encrypted, deduplicated)
**Backup Destination**: NAS at `/mnt/nas/backups/homelab-restic`
**Backup Frequency**: Daily (can be automated with systemd timer)
**Encryption**: AES-256, password stored in `/home/hoborg/creds/restic-password.txt`
## What Gets Backed Up
### Full System Backup Includes:
1. **Package Lists**
- All explicitly installed packages (pacman)
- All installed packages (including dependencies)
- AUR/foreign packages list
- Allows exact system recreation after reinstall
2. **System Configuration** (`/etc/`)
- Nginx configuration
- Systemd services
- Network configuration
- All system-wide configs
- Excludes: shadow/gshadow password files
3. **Docker Volumes**
- Gitea (Git repositories and database)
- Jellyfin (media library configuration)
- Nextcloud (cloud storage data and database)
- Portainer (container management data)
### What's NOT Backed Up (Intentionally):
- **User data in `/home`**: Separate partition, safe from system reinstalls
- **Media files**: Already stored on NAS (Music, Videos, Pictures)
- **User documents**: Synced via Nextcloud/Copyparty or on separate partition
## Setup
### Initial Setup (One-Time)
1. **Initialize the backup repository:**
```bash
sudo /home/hoborg/homelab/scripts/backup-init.sh
```
This will:
- Install Restic if needed
- Create backup directory on NAS (`/mnt/nas/backups/homelab-restic`)
- Generate encryption password (saved to `/home/hoborg/creds/restic-password.txt`)
- Initialize the Restic repository
⚠️ **IMPORTANT**: Back up the password file! Without it, backups cannot be restored.
### Running Backups
**Manual backup:**
```bash
sudo /home/hoborg/homelab/scripts/backup-system-full.sh
```
**Automated backups** (optional - setup systemd timer later):
- Create systemd service and timer
- Schedule daily backups (e.g., 3 AM)
- Logs written to `/var/log/homelab-backup.log`
## Backup Scripts
### `backup-init.sh`
Initializes the Restic repository. Run once during setup.
Location: `/home/hoborg/homelab/scripts/backup-init.sh`
### `backup-system-full.sh`
Performs full system backup including packages, configs, and Docker volumes.
Location: `/home/hoborg/homelab/scripts/backup-system-full.sh`
What it does:
1. Exports package lists (pacman explicit, all packages, AUR packages)
2. Exports Docker volumes to compressed tarballs
3. Backs up entire `/etc/` directory
4. Prunes old backups (keeps: 7 daily, 4 weekly, 6 monthly, 2 yearly)
5. Shows backup statistics
## Restore Procedures
### List Available Backups
```bash
export RESTIC_PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
restic -r /mnt/nas/backups/homelab-restic snapshots
```
### Restore After System Reinstall
1. **Restore backup to temporary location:**
```bash
export RESTIC_PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
restic -r /mnt/nas/backups/homelab-restic restore latest --target /tmp/restore
```
2. **Reinstall packages:**
```bash
# Reinstall all explicitly installed packages
sudo pacman -S --needed $(cat /tmp/restore/tmp/system-backup-*/pacman-explicit.txt)
# Reinstall AUR packages
yay -S --needed $(cat /tmp/restore/tmp/system-backup-*/aur-packages.txt)
```
3. **Restore system configs:**
```bash
sudo cp -r /tmp/restore/etc/* /etc/
sudo systemctl daemon-reload
```
4. **Restore Docker volumes:**
```bash
# Example: Restore Gitea
docker-compose -f /opt/docker/gitea/docker-compose.yml down
sudo tar xzf /tmp/restore/tmp/docker-backup-*/gitea-data.tar.gz -C /tmp/gitea-restore
docker run --rm -v gitea_gitea:/data -v /tmp/gitea-restore:/backup alpine \
sh -c "rm -rf /data/* && cp -a /backup/* /data/"
docker-compose -f /opt/docker/gitea/docker-compose.yml up -d
```
### Rollback After Broken Update
If a system update breaks something:
1. **Restore specific config:**
```bash
export RESTIC_PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
# Restore nginx config
restic -r /mnt/nas/backups/homelab-restic restore latest \
--target /tmp/restore --include /etc/nginx/
sudo cp -r /tmp/restore/etc/nginx/* /etc/nginx/
sudo systemctl reload nginx
```
2. **Downgrade packages:**
```bash
# Check which packages were updated
grep -A5 "upgraded" /var/log/pacman.log | tail -20
# Downgrade specific package to cached version
sudo pacman -U /var/cache/pacman/pkg/package-old-version.pkg.tar.zst
```
### Restore Specific Docker Volume
```bash
export RESTIC_PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
# List files in backup
restic -r /mnt/nas/backups/homelab-restic ls latest | grep docker-backup
# Restore specific volume tarball
restic -r /mnt/nas/backups/homelab-restic restore latest \
--target /tmp/restore \
--include /tmp/docker-backup-*/nextcloud-data.tar.gz
# Extract and restore to Docker volume
mkdir /tmp/nextcloud-data
tar xzf /tmp/restore/tmp/docker-backup-*/nextcloud-data.tar.gz -C /tmp/nextcloud-data
docker run --rm -v nextcloud_nextcloud_data:/data -v /tmp/nextcloud-data:/backup alpine \
sh -c "rm -rf /data/* && cp -a /backup/* /data/"
```
## Backup Retention Policy
The backup script automatically prunes old backups:
- **Daily**: Keep last 7 days
- **Weekly**: Keep last 4 weeks
- **Monthly**: Keep last 6 months
- **Yearly**: Keep last 2 years
This balances storage space with recovery options.
## Troubleshooting
### "Repository not initialized" Error
Run the initialization script:
```bash
sudo /home/hoborg/homelab/scripts/backup-init.sh
```
### "Password incorrect" Error
Check that password file exists and is readable:
```bash
ls -l /home/hoborg/creds/restic-password.txt
```
### Check Backup Size
```bash
export RESTIC_PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
restic -r /mnt/nas/backups/homelab-restic stats --mode restore-size
```
### Verify Backup Integrity
```bash
export RESTIC_PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
restic -r /mnt/nas/backups/homelab-restic check
```
## Security Notes
- Backups are encrypted with AES-256
- Password file has `600` permissions (owner read/write only)
- Shadow/gshadow files are excluded from backups
- NAS should have access restrictions
- Consider offsite backup copy of password file
## Future Enhancements
- [ ] Automated systemd timer setup
- [ ] Pre-backup notifications
- [ ] Post-backup success/failure notifications
- [ ] Backup verification script
- [ ] Web dashboard for backup status
- [ ] Offsite backup replication (optional)

View File

@@ -1,5 +1,7 @@
# Voice Assistant Setup # Voice Assistant Setup
⚠️ **STATUS: DISABLED** - onnxruntime package was removed to free disk space (1.2GB). Voice functionality is currently unavailable.
This document describes how to set up AI voice capabilities for Claude Code using local TTS (Text-to-Speech) services. This document describes how to set up AI voice capabilities for Claude Code using local TTS (Text-to-Speech) services.
## Overview ## Overview
@@ -8,6 +10,7 @@ The voice assistant setup uses:
- **Piper TTS**: Local neural text-to-speech engine for generating natural-sounding speech - **Piper TTS**: Local neural text-to-speech engine for generating natural-sounding speech
- **FastAPI**: HTTP server wrapper to make Piper compatible with voice-mode - **FastAPI**: HTTP server wrapper to make Piper compatible with voice-mode
- **Ryan voice model**: Professional male US English voice for AI assistant personality - **Ryan voice model**: Professional male US English voice for AI assistant personality
- **onnxruntime**: ML inference library (removed - required for TTS)
## Prerequisites ## Prerequisites

95
scripts/backup-homelab.sh Executable file
View File

@@ -0,0 +1,95 @@
#!/bin/bash
# Backup Docker volumes and system configs to NAS using Restic
# Can be run manually or via systemd timer
set -e
# Configuration
BACKUP_REPO="/mnt/nas/backups/homelab-restic"
PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
LOG_FILE="/var/log/homelab-backup.log"
DOCKER_BACKUP_DIR="/tmp/docker-backup-$$"
# Export password
export RESTIC_PASSWORD_FILE="$PASSWORD_FILE"
# Logging function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "========================================"
log "Starting Homelab Backup"
log "========================================"
# Check if repository exists
if ! restic -r "$BACKUP_REPO" snapshots &>/dev/null; then
log "ERROR: Backup repository not initialized. Run backup-init.sh first."
exit 1
fi
# Create temporary directory for Docker volume exports
mkdir -p "$DOCKER_BACKUP_DIR"
log "Created temporary backup directory: $DOCKER_BACKUP_DIR"
# Export Docker volumes
log "Exporting Docker volumes..."
# Gitea
log " - Exporting Gitea data..."
docker run --rm -v gitea_gitea:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/gitea-data.tar.gz -C /data .
# Jellyfin
log " - Exporting Jellyfin config..."
docker run --rm -v jellyfin_jellyfin_config:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/jellyfin-config.tar.gz -C /data .
# Nextcloud data
log " - Exporting Nextcloud data..."
docker run --rm -v nextcloud_nextcloud_data:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/nextcloud-data.tar.gz -C /data .
# Nextcloud database
log " - Exporting Nextcloud database..."
docker run --rm -v nextcloud_nextcloud_db:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/nextcloud-db.tar.gz -C /data .
# Portainer
log " - Exporting Portainer data..."
docker run --rm -v portainer_data:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/portainer-data.tar.gz -C /data .
log "✓ Docker volumes exported"
# Run backup
log "Running Restic backup..."
restic -r "$BACKUP_REPO" backup \
--verbose \
--tag homelab-docker \
--tag homelab-configs \
/etc/nginx/sites-available/homelab \
/etc/systemd/system/copyparty.service \
/home/hoborg/.config/copyparty/ \
"$DOCKER_BACKUP_DIR"
# Cleanup temporary files
log "Cleaning up temporary files..."
rm -rf "$DOCKER_BACKUP_DIR"
# Prune old backups (keep: 7 daily, 4 weekly, 6 monthly, 2 yearly)
log "Pruning old backups..."
restic -r "$BACKUP_REPO" forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--keep-yearly 2 \
--prune
# Show backup stats
log "Backup statistics:"
restic -r "$BACKUP_REPO" stats --mode restore-size
log "========================================"
log "Backup completed successfully!"
log "========================================"

74
scripts/backup-init.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
# Initialize Restic backup repository on NAS
# Run once to set up the backup system
set -e
# Configuration
BACKUP_REPO="/mnt/nas/backups/homelab-restic"
PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
echo "========================================"
echo "Restic Backup Repository Initialization"
echo "========================================"
echo ""
echo "This will:"
echo " 1. Install restic if needed"
echo " 2. Create backup repository at: $BACKUP_REPO"
echo " 3. Generate and save encryption password"
echo ""
# Check if restic is installed
if ! command -v restic &> /dev/null; then
echo "Installing restic..."
pacman -S --needed --noconfirm restic
echo "✓ Restic installed"
else
echo "✓ Restic already installed ($(restic version))"
fi
# Create backup directory on NAS
echo ""
echo "Creating backup directory on NAS..."
mkdir -p "$BACKUP_REPO"
echo "✓ Directory created: $BACKUP_REPO"
# Generate random password if doesn't exist
echo ""
if [ -f "$PASSWORD_FILE" ]; then
echo "✓ Password file already exists: $PASSWORD_FILE"
else
echo "Generating encryption password..."
mkdir -p /home/hoborg/creds
openssl rand -base64 32 > "$PASSWORD_FILE"
chmod 600 "$PASSWORD_FILE"
echo "✓ Password saved to: $PASSWORD_FILE"
echo "⚠️ IMPORTANT: Back up this password file! Without it, backups cannot be restored."
fi
# Export password for restic
export RESTIC_PASSWORD_FILE="$PASSWORD_FILE"
# Initialize repository
echo ""
echo "Initializing Restic repository..."
if restic -r "$BACKUP_REPO" snapshots &>/dev/null; then
echo "✓ Repository already initialized"
else
restic -r "$BACKUP_REPO" init
echo "✓ Repository initialized"
fi
echo ""
echo "========================================"
echo "Backup Repository Ready!"
echo "========================================"
echo ""
echo "Repository location: $BACKUP_REPO"
echo "Password file: $PASSWORD_FILE"
echo ""
echo "Next steps:"
echo " 1. Run backup-homelab.sh to create first backup"
echo " 2. Test restore with backup-restore.sh"
echo " 3. Enable automated backups with systemd timer"
echo ""

128
scripts/backup-system-full.sh Executable file
View File

@@ -0,0 +1,128 @@
#!/bin/bash
# Full system backup - packages, configs, and Docker volumes
# Can be run manually or via systemd timer
set -e
# Configuration
BACKUP_REPO="/mnt/nas/backups/homelab-restic"
PASSWORD_FILE="/home/hoborg/creds/restic-password.txt"
LOG_FILE="/var/log/homelab-backup.log"
DOCKER_BACKUP_DIR="/tmp/docker-backup-$$"
SYSTEM_BACKUP_DIR="/tmp/system-backup-$$"
# Export password
export RESTIC_PASSWORD_FILE="$PASSWORD_FILE"
# Logging function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "========================================"
log "Starting Full System Backup"
log "========================================"
# Check if repository exists
if ! restic -r "$BACKUP_REPO" snapshots &>/dev/null; then
log "ERROR: Backup repository not initialized. Run backup-init.sh first."
exit 1
fi
# Create temporary directories
mkdir -p "$DOCKER_BACKUP_DIR"
mkdir -p "$SYSTEM_BACKUP_DIR"
log "Created temporary directories"
# ============================================
# 1. Export Package Lists
# ============================================
log "Exporting package lists..."
pacman -Qqe > "$SYSTEM_BACKUP_DIR/pacman-explicit.txt" # Explicitly installed packages
pacman -Qq > "$SYSTEM_BACKUP_DIR/pacman-all.txt" # All installed packages
pacman -Qqm > "$SYSTEM_BACKUP_DIR/aur-packages.txt" # AUR/foreign packages only
log "✓ Package lists exported ($(wc -l < $SYSTEM_BACKUP_DIR/pacman-explicit.txt) explicit, $(wc -l < $SYSTEM_BACKUP_DIR/aur-packages.txt) AUR)"
# ============================================
# 2. Export Docker Volumes
# ============================================
log "Exporting Docker volumes..."
# Gitea
log " - Exporting Gitea data..."
docker run --rm -v gitea_gitea:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/gitea-data.tar.gz -C /data .
# Jellyfin
log " - Exporting Jellyfin config..."
docker run --rm -v jellyfin_jellyfin_config:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/jellyfin-config.tar.gz -C /data .
# Nextcloud data
log " - Exporting Nextcloud data..."
docker run --rm -v nextcloud_nextcloud_data:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/nextcloud-data.tar.gz -C /data .
# Nextcloud database
log " - Exporting Nextcloud database..."
docker run --rm -v nextcloud_nextcloud_db:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/nextcloud-db.tar.gz -C /data .
# Portainer
log " - Exporting Portainer data..."
docker run --rm -v portainer_data:/data -v "$DOCKER_BACKUP_DIR":/backup alpine \
tar czf /backup/portainer-data.tar.gz -C /data .
log "✓ Docker volumes exported"
# ============================================
# 3. Run Backup
# ============================================
log "Running Restic backup..."
restic -r "$BACKUP_REPO" backup \
--verbose \
--tag full-system \
--exclude '/etc/shadow*' \
--exclude '/etc/gshadow*' \
--exclude '/etc/passwd-' \
--exclude '/etc/group-' \
/etc/ \
"$DOCKER_BACKUP_DIR" \
"$SYSTEM_BACKUP_DIR"
# ============================================
# 4. Cleanup
# ============================================
log "Cleaning up temporary files..."
rm -rf "$DOCKER_BACKUP_DIR"
rm -rf "$SYSTEM_BACKUP_DIR"
# ============================================
# 5. Prune Old Backups
# ============================================
log "Pruning old backups..."
restic -r "$BACKUP_REPO" forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 3 \
--keep-yearly 1 \
--prune
# ============================================
# 6. Show Stats
# ============================================
log "Backup statistics:"
restic -r "$BACKUP_REPO" stats --mode restore-size
log "========================================"
log "Full system backup completed!"
log "========================================"
log ""
log "Backed up:"
log " - /etc/ (all system configs)"
log " - Docker volumes (Gitea, Jellyfin, Nextcloud, Portainer)"
log " - Package lists (pacman + AUR)"
log ""
log "To restore packages after reinstall:"
log " pacman -S --needed \$(cat pacman-explicit.txt)"
log " yay -S --needed \$(cat aur-packages.txt)"

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Permanent ban script for IPs that have been banned more than 4 times by fail2ban
# Run via cron: 0 */6 * * * /home/hoborg/homelab/scripts/permanent-ban-repeat-offenders.sh
LOGFILE="/var/log/permanent-ban.log"
BANLIST_FILE="/etc/fail2ban/permanent-banlist.conf"
THRESHOLD=4
# Function to log with timestamp
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOGFILE"
}
# Ensure permanent banlist file exists
if [ ! -f "$BANLIST_FILE" ]; then
touch "$BANLIST_FILE"
chmod 644 "$BANLIST_FILE"
fi
log_message "Starting repeat offender analysis (threshold: $THRESHOLD bans)"
# Get list of IPs with ban counts from fail2ban logs
REPEAT_OFFENDERS=$(grep "Ban " /var/log/fail2ban.log* | \
grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | \
sort | uniq -c | sort -nr | \
awk -v threshold="$THRESHOLD" '$1 > threshold {print $2 " " $1}')
if [ -z "$REPEAT_OFFENDERS" ]; then
log_message "No repeat offenders found above threshold"
exit 0
fi
# Process each repeat offender
echo "$REPEAT_OFFENDERS" | while read ip ban_count; do
# Skip if already permanently banned
if grep -q "^$ip" "$BANLIST_FILE"; then
log_message "IP $ip already permanently banned (skip)"
continue
fi
# Get country information
COUNTRY=$(whois "$ip" 2>/dev/null | grep -i "country:" | head -1 | awk '{print $2}')
# Add to permanent banlist
echo "$ip # Banned $(date '+%Y-%m-%d') - $ban_count offenses - Country: $COUNTRY" >> "$BANLIST_FILE"
# Add permanent iptables rule
iptables -I INPUT -s "$ip" -j DROP
# Make iptables rule persistent (systemd approach)
if command -v iptables-save >/dev/null 2>&1; then
iptables-save > /etc/iptables/iptables.rules 2>/dev/null || true
fi
log_message "PERMANENTLY BANNED: $ip ($ban_count offenses, Country: $COUNTRY)"
done
# Reload fail2ban to ensure it sees the updated banlist
if systemctl is-active --quiet fail2ban; then
log_message "Reloading fail2ban service"
systemctl reload fail2ban
fi
log_message "Repeat offender analysis completed"

130
scripts/setup-docker-symlinks.sh Executable file
View File

@@ -0,0 +1,130 @@
#!/bin/bash
# Set up symlinks for Docker configurations to avoid manual copying
set -e # Exit on error
echo "==================================================================="
echo "Docker Configuration Symlink Setup"
echo "==================================================================="
echo ""
REPO_ROOT="/home/hoborg/homelab"
# Function to create symlink with backup
create_symlink() {
local service=$1
local target=$2
local link=$3
echo "-------------------------------------------------------------------"
echo "Setting up: $service"
echo "-------------------------------------------------------------------"
echo "Target: $target"
echo "Link: $link"
echo ""
# Check if target exists
if [ ! -f "$target" ]; then
echo "ERROR: Target file does not exist: $target"
return 1
fi
# Create parent directory if it doesn't exist
local link_dir=$(dirname "$link")
if [ ! -d "$link_dir" ]; then
echo "Creating directory: $link_dir"
mkdir -p "$link_dir"
fi
# Backup existing file if it's not already a symlink
if [ -f "$link" ] && [ ! -L "$link" ]; then
backup="${link}.backup.$(date +%Y%m%d_%H%M%S)"
echo "Backing up existing file to: $backup"
cp "$link" "$backup"
rm "$link"
elif [ -L "$link" ]; then
echo "Removing existing symlink"
rm "$link"
fi
# Create symlink
ln -s "$target" "$link"
if [ -L "$link" ]; then
echo "✓ Symlink created successfully"
else
echo "✗ Failed to create symlink"
return 1
fi
echo ""
}
# Gitea docker-compose.yml
create_symlink "Gitea docker-compose" \
"$REPO_ROOT/config/docker/gitea/docker-compose.yml" \
"/opt/docker/gitea/docker-compose.yml"
# Jellyfin docker-compose.yml
create_symlink "Jellyfin docker-compose" \
"$REPO_ROOT/config/docker/jellyfin/docker-compose.yml" \
"/opt/docker/jellyfin/docker-compose.yml"
# qBittorrent docker-compose.yml
create_symlink "qBittorrent docker-compose" \
"$REPO_ROOT/config/docker/qbittorrent/docker-compose.yml" \
"/opt/docker/qbittorrent/docker-compose.yml"
# Portainer docker-compose.yml
create_symlink "Portainer docker-compose" \
"$REPO_ROOT/config/docker/portainer/docker-compose.yml" \
"/opt/docker/portainer/docker-compose.yml"
# Copyparty docker-compose.yml
create_symlink "Copyparty docker-compose" \
"$REPO_ROOT/config/docker/copyparty/docker-compose.yml" \
"/opt/docker/copyparty/docker-compose.yml"
# Glances docker-compose.yml
create_symlink "Glances docker-compose" \
"$REPO_ROOT/config/docker/glances/docker-compose.yml" \
"/opt/docker/glances/docker-compose.yml"
# Syncthing docker-compose.yml
create_symlink "Syncthing docker-compose" \
"$REPO_ROOT/config/docker/syncthing/docker-compose.yml" \
"/opt/docker/syncthing/docker-compose.yml"
# Docker daemon config
create_symlink "Docker daemon config" \
"$REPO_ROOT/config/docker/daemon.json" \
"/etc/docker/daemon.json"
# Landing page
create_symlink "Landing page" \
"$REPO_ROOT/config/www/index.html" \
"/var/www/homelab/index.html"
echo "==================================================================="
echo "Summary"
echo "==================================================================="
echo ""
echo "✓ All symlinks created successfully!"
echo ""
echo "Notes:"
echo " - Original files backed up with timestamp"
echo " - Edit files in $REPO_ROOT/config/docker/"
echo " - Changes take effect immediately (no copying needed)"
echo " - For daemon.json: restart docker after changes"
echo " - For docker-compose: restart containers after changes"
echo ""
echo "Verify symlinks:"
echo " ls -l /opt/docker/gitea/docker-compose.yml"
echo " ls -l /opt/docker/jellyfin/docker-compose.yml"
echo " ls -l /opt/docker/qbittorrent/docker-compose.yml"
echo " ls -l /opt/docker/portainer/docker-compose.yml"
echo " ls -l /opt/docker/copyparty/docker-compose.yml"
echo " ls -l /opt/docker/glances/docker-compose.yml"
echo " ls -l /opt/docker/syncthing/docker-compose.yml"
echo " ls -l /etc/docker/daemon.json"
echo ""