Compare commits

...

20 Commits

Author SHA1 Message Date
1f3f111d88 Add metrics monitoring stack with VictoriaMetrics, Grafana, and node_exporter
Implement complete monitoring infrastructure following rick-infra principles:

Components:
- VictoriaMetrics: Prometheus-compatible TSDB (7x less RAM usage)
- Grafana: Visualization dashboard with Authentik OAuth/OIDC integration
- node_exporter: System metrics collection (CPU, memory, disk, network)

Architecture:
- All services run as native systemd binaries (no containers)
- localhost-only binding for security
- Grafana uses native OAuth integration with Authentik (not forward_auth)
- Full systemd security hardening enabled
- Proxied via Caddy at metrics.jnss.me with HTTPS

Role Features:
- Unified metrics role (single role for complete stack)
- Automatic role mapping via Authentik groups:
  - authentik Admins OR grafana-admins -> Admin access
  - grafana-editors -> Editor access
  - All others -> Viewer access
- VictoriaMetrics auto-provisioned as default Grafana datasource
- 12-month metrics retention by default
- Comprehensive documentation included

Security:
- OAuth/OIDC SSO via Authentik
- All metrics services bind to 127.0.0.1 only
- systemd hardening (NoNewPrivileges, ProtectSystem, etc.)
- Grafana accessible only via Caddy HTTPS proxy

Documentation:
- roles/metrics/README.md: Complete role documentation
- docs/metrics-deployment-guide.md: Step-by-step deployment guide

Configuration:
- Updated rick-infra.yml to include metrics deployment
- Grafana port set to 3001 (Gitea uses 3000)
- Ready for multi-host expansion (designed for future node_exporter deployment to production hosts)
2025-12-28 19:18:30 +01:00
eca3b4a726 removing backups of caddy configs 2025-12-28 18:19:05 +01:00
9bb9405b90 added jnss-web 2025-12-23 23:36:51 +01:00
bfd6f22f0e Add Vaultwarden password manager role with PostgreSQL and SSO support
- Implement complete Vaultwarden deployment using Podman Quadlet
- PostgreSQL backend via Unix socket with 777 permissions
- Caddy reverse proxy with WebSocket support for live sync
- Control-node admin token hashing using argon2 (OWASP preset)
- Idempotent token hashing with deterministic salt generation
- Full Authentik SSO integration following official guide
- SMTP email configuration support (optional)
- Invitation-only user registration by default
- Comprehensive documentation with setup and troubleshooting guides

Technical Details:
- Container: vaultwarden/server:latest from Docker Hub
- Database: PostgreSQL via /var/run/postgresql socket
- Port: 8080 (localhost only, proxied by Caddy)
- Domain: vault.jnss.me
- Admin token: Hashed on control node with argon2id
- SSO: OpenID Connect with offline_access scope support

Role includes automatic argon2 installation on control node if needed.
2025-12-22 21:33:27 +01:00
89b43180fc Refactor Nextcloud configuration to use OCC script approach and add email/OIDC support
Major architectural changes:
- Replace config file templating with unified OCC command script
- Remove custom_apps mount overlay that caused Caddy serving issues
- Implement script-based configuration for idempotency and clarity

Configuration improvements:
- Add email/SMTP support with master switch (nextcloud_email_enabled)
- Add OIDC/SSO integration with Authentik support
- Add apps installation (user_oidc, calendar, contacts)
- Enable group provisioning and quota management from OIDC
- Set nextcloud_oidc_unique_uid to false per Authentik docs

Files removed:
- nextcloud.config.php.j2 (replaced by OCC commands)
- redis.config.php.j2 (replaced by OCC commands)
- optimization.yml (merged into configure.yml)

Files added:
- configure-nextcloud.sh.j2 (single source of truth for config)
- configure.yml (deploys and runs configuration script)

Documentation:
- Add comprehensive OIDC setup guide with Authentik integration
- Document custom scope mapping and group provisioning
- Add email configuration examples for common providers
- Update vault variables documentation
- Explain two-phase deployment approach

Host configuration:
- Change admin user from 'admin' to 'joakim'
- Add admin email configuration
2025-12-21 14:54:44 +01:00
846ab74f87 Fix Nextcloud DNS resolution and implement systemd cron for background jobs
- Enable IP forwarding in security playbook (net.ipv4.ip_forward = 1)
- Add podman network firewall rules to fix container DNS/HTTPS access
- Implement systemd timer for reliable Nextcloud background job execution
- Add database optimization tasks (indices, bigint conversion, mimetypes)
- Configure maintenance window (04:00 UTC) and phone region (NO)
- Add security headers (X-Robots-Tag, X-Permitted-Cross-Domain-Policies)
- Create Nextcloud removal playbook for clean uninstall
- Fix nftables interface matching (podman0 vs podman+)

Root cause: nftables FORWARD chain blocked container egress traffic
Solution: Explicit firewall rules for podman0 bridge interface
2025-12-20 19:51:26 +01:00
90bbcd97b1 Add Gitea email configuration and document SMTP authentication troubleshooting
Changes:
- Configure Gitea mailer with Titan Email SMTP settings
- Add SMTP_AUTH = PLAIN for authentication method specification
- Update SMTP password in vault (vault_gitea_smtp_password)

Email Status:
Currently non-functional due to SMTP authentication rejection by Titan Email
servers. Error: 535 5.7.8 authentication failed

Troubleshooting Performed:
- Tested both port 587 (STARTTLS) and 465 (SSL/TLS)
- Verified credentials work in webmail
- Tested AUTH PLAIN and AUTH LOGIN methods
- Removed conflicting TLS settings
- Both authentication methods rejected despite correct credentials

Root Cause:
The issue is NOT a Gitea configuration problem. Titan Email SMTP server
is rejecting all authentication attempts from the VPS (69.62.119.31)
despite credentials being correct and working in webmail.

Possible causes:
- SMTP access may need to be enabled in Hostinger control panel
- VPS IP may require whitelisting
- Account may need additional verification for SMTP access
- Titan Email plan may not include external SMTP access

Documentation:
Created comprehensive troubleshooting guide at:
docs/gitea-email-troubleshooting.md

Files Modified:
- roles/gitea/templates/app.ini.j2 (+1 line: SMTP_AUTH = PLAIN)
- docs/gitea-email-troubleshooting.md (new file, complete troubleshooting log)
- host_vars/arch-vps/vault.yml (updated SMTP password - not committed)

Next Steps:
- Check Hostinger control panel for SMTP/IMAP access toggle
- Test SMTP from different IP to rule out IP blocking
- Contact Hostinger/Titan support for SMTP access verification
- Consider alternative email providers if Titan SMTP unavailable
2025-12-19 21:25:14 +01:00
1be7122251 Update task list - Gitea OAuth and registration configuration complete 2025-12-18 21:09:47 +01:00
467e79c84b Configure Gitea as private OAuth-enabled Git server with email support
Major Changes:
- Configure private Git server with OAuth-preferred authentication
- Integrate Titan Email for notifications and OAuth workflows
- Enable CI/CD Actions and repository mirroring
- Implement enhanced security hardening

Authentication & Access Control:
- Require sign-in for all access (unauthorized users blocked)
- OAuth via Authentik as primary login method (password form hidden)
- Password authentication still functional as backup via direct URL
- Registration disabled (admin-only user creation)
- Auto-registration for OAuth users with account linking support

Email Configuration (Titan Email):
- SMTP: smtp.titan.email:587 (STARTTLS)
- From address: hello@jnss.me
- Used for: OAuth account linking, notifications, confirmations
- Subject prefix: [Gitea]

Repository Privacy & Features:
- Private repositories by default (public repos allowed)
- Unauthorized users cannot view any content (must sign in)
- External integrations disabled (ext_issues, ext_wiki)
- Manual repository creation required (no push-to-create)
- LFS enabled for large file storage

Features Enabled:
- CI/CD Actions with GitHub actions support
- Repository mirroring (pull/push mirrors enabled)
- User organization creation
- Webhook security (restricted to private/loopback)

Security Enhancements:
- HTTPS-only session cookies with strict SameSite policy
- CSRF cookie HTTP-only protection
- Password breach checking (HaveIBeenPwned)
- 1-hour session timeout (reduced from 24h)
- Reverse proxy trust limited to Caddy only
- API Swagger docs disabled in production

Configuration Sections Added:
- [oauth2_client] - OAuth integration settings
- [mailer] - Email/SMTP configuration
- [session] - Enhanced session security
- [actions] - CI/CD workflow configuration
- [mirror] - Repository mirroring settings
- [api] - API access configuration
- [webhook] - Webhook security restrictions
- [service.explore] - Public content settings

Files Changed:
- roles/gitea/defaults/main.yml: +97 lines (OAuth, email, security vars)
- roles/gitea/templates/app.ini.j2: +94 lines (config sections)
- host_vars/arch-vps/vault.yml: +1 line (SMTP password - not committed)

Deployment Status:
- Successfully deployed to arch-vps
- Service running and healthy
- Ready for OAuth provider configuration in Authentik
- Tested: HTTP access, configuration generation, service health
2025-12-18 21:09:31 +01:00
cf71fb3a8d Implement SSH passthrough mode and refactor Gitea domain configuration
Major Changes:
- Add dual SSH mode system (passthrough default, dedicated fallback)
- Refactor domain configuration to use direct specification pattern
- Fix critical fail2ban security gap in dedicated mode
- Separate HTTP and SSH domains for cleaner Git URLs
2025-12-17 21:51:24 +01:00
2fe194ba82 Implement modular nftables architecture and Gitea SSH firewall management
- Restructure security playbook with modular nftables loader
- Base rules loaded first, service rules second, drop rule last
- Add Gitea self-contained firewall management (port 2222)
- Add fail2ban protection for Gitea SSH brute force attacks
- Update documentation with new firewall architecture
- Create comprehensive Gitea deployment and testing guide

This enables self-contained service roles to manage their own firewall
rules without modifying the central security playbook. Each service
deploys rules to /etc/nftables.d/ which are loaded before the final
drop rule, maintaining the defense-in-depth security model.
2025-12-16 21:45:22 +01:00
9b12225ec8 MILE: All current services confirmed working on a fresh arch vps 2025-12-16 20:40:25 +01:00
f40349c2e7 update authentik handlers
authentik-pod handles container orchestration, so no server and worker
handlers needed
2025-12-16 20:39:36 +01:00
4f8b46fa14 solve folder structure issue 2025-12-16 20:38:51 +01:00
a9f814d929 Add comprehensive devigo deployment documentation
- Complete deployment guide with architecture overview
- Documented all deployment steps and current status
- Added lessons learned from deployment challenges
- Comprehensive troubleshooting guide
- Maintenance procedures and security considerations
- Migration notes from arch-vps to mini-vps

Key sections:
- OAuth flow and authentication
- CI/CD deployment pipeline
- MIME type handling strategy
- Podman authentication for systemd
- Future improvements and technical debt tracking
2025-12-16 00:53:45 +01:00
44584c68f1 Add GitHub Container Registry authentication to Podman role
- Deploy /etc/containers/auth.json with GHCR credentials
- Support for private container image pulls
- Credentials encrypted in Ansible vault
- Used by devigo and other services pulling from private registries
- Updated documentation with authentication setup
2025-12-16 00:53:42 +01:00
0ecbb84fa5 Configure devigo service in production environment
- Added devigo role to production playbook
- Configured domains: devigo.no (apex), www.devigo.no, decap.jnss.me
- Set OAuth trusted origins for multi-domain support
- Integrated with existing Caddy and Podman infrastructure
2025-12-16 00:53:39 +01:00
1350d10a7c Add devigo deployment role for mini-vps production environment
- Created comprehensive devigo Ansible role with Podman Quadlet support
- Deployed devigo-site container (Hugo + nginx) via systemd
- Deployed devigo-decap-oauth OAuth2 proxy for Decap CMS
- Integrated with Caddy reverse proxy for HTTPS

Services deployed:
- devigo.no (apex domain, primary)
- www.devigo.no (redirects to apex)
- decap.jnss.me (OAuth proxy)

Key features:
- REGISTRY_AUTH_FILE environment for Podman GHCR authentication
- TRUSTED_ORIGINS (plural) for decapcms-oauth2 multi-origin support
- JavaScript-based Decap CMS initialization (eliminates YAML MIME dependency)
- nginx location block for YAML MIME type (text/yaml)
- Automated deployment via GitHub Actions CI/CD
- Comprehensive documentation with troubleshooting guide
- Architecture decision records

Fixes applied during deployment:
- OAuth origin trust validation (TRUSTED_ORIGINS vs TRUSTED_ORIGIN)
- MIME type handling strategy (location-specific vs server-level types block)
- Decap CMS initialization method (JavaScript vs link tag)
- Podman authentication for systemd services (REGISTRY_AUTH_FILE)

Testing status:
-  MIME types verified (HTML, CSS, YAML all correct)
-  OAuth authentication working
-  Container image pulls from private GHCR
-  Automated deployments functional
-  Site fully operational at devigo.no
2025-12-16 00:53:33 +01:00
ecbeb07ba2 Migrate sigvild-gallery to production environment
- Add multi-environment architecture (homelab + production)
- Create production environment (mini-vps) for client projects
- Create homelab playbook for arch-vps services
- Create production playbook for mini-vps services
- Move sigvild-gallery from homelab to production
- Restructure variables: group_vars/production + host_vars/arch-vps
- Add backup-sigvild.yml playbook with auto-restore functionality
- Fix restore logic to check for data before creating directories
- Add manual variable loading workaround for Ansible 2.20
- Update all documentation for multi-environment setup
- Add ADR-007 documenting multi-environment architecture decision
2025-12-15 16:33:33 +01:00
e8b76c6a72 Update authentication documentation to reflect OAuth/OIDC as primary method
- Update architecture-decisions.md: Change decision to OAuth/OIDC primary, forward auth fallback
  - Add comprehensive OAuth/OIDC and forward auth flow diagrams
  - Add decision matrix comparing both authentication methods
  - Include real examples: Nextcloud/Gitea OAuth configs, whoami forward auth
  - Update rationale to emphasize OAuth/OIDC security and standards benefits

- Update authentication-architecture.md: Align with new OAuth-first approach
  - Add 'Choosing the Right Pattern' section with clear decision guidance
  - Swap pattern order: OAuth/OIDC (Pattern 1), Forward Auth (Pattern 2)
  - Update Example 1: Change Gitea from forward auth to OAuth/OIDC integration
  - Add emphasis on primary vs fallback methods throughout

- Update authentik-deployment-guide.md: Reflect OAuth/OIDC preference
  - Update overview to mention OAuth2/OIDC provider and forward auth fallback
  - Add decision guidance to service integration examples
  - Reorder examples: Nextcloud OAuth (primary), forward auth (fallback)
  - Clarify forward auth should only be used for services without OAuth support

This update ensures all authentication documentation consistently reflects the
agreed architectural decision: use OAuth/OIDC when services support it
(Nextcloud, Gitea, modern apps), and only use forward auth as a fallback for
legacy applications, static sites, or simple tools without OAuth capabilities.
2025-12-15 00:25:24 +01:00
108 changed files with 9407 additions and 1135 deletions

View File

@@ -32,43 +32,64 @@ Rick-infra implements a security-first infrastructure stack featuring:
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
## Infrastructure Environments
Rick-infra manages **two separate environments**:
### 🏠 Homelab (arch-vps)
Personal services and experimentation at **jnss.me**:
- PostgreSQL, Valkey, Podman infrastructure
- Authentik SSO (auth.jnss.me)
- Nextcloud (cloud.jnss.me)
- Gitea (git.jnss.me)
### 🚀 Production (mini-vps)
Client projects requiring high uptime:
- Sigvild Gallery (sigvild.no, api.sigvild.no)
- Minimal infrastructure footprint
## Quick Start ## Quick Start
### Prerequisites ### Prerequisites
- **VPS**: Fresh Arch Linux VPS with root access - **VPS**: Fresh Arch Linux VPS with root access
- **DNS**: Domain pointed to VPS IP address - **DNS**: Domains pointed to VPS IP addresses
- **SSH**: Key-based authentication configured - **SSH**: Key-based authentication configured
### Deploy Complete Stack ### Deploy Infrastructure
```bash ```bash
# 1. Clone repository # 1. Clone repository
git clone https://github.com/your-username/rick-infra.git git clone https://github.com/your-username/rick-infra.git
cd rick-infra cd rick-infra
# 2. Configure inventory # 2. Configure inventory (already set up)
cp inventory/hosts.yml.example inventory/hosts.yml # inventory/hosts.yml defines homelab and production groups
# Edit inventory/hosts.yml with your VPS details
# 3. Set up vault variables # 3. Set up vault variables
ansible-vault create host_vars/arch-vps/vault.yml ansible-vault edit group_vars/production/vault.yml # Production secrets
# Add required secrets (see deployment guide) ansible-vault edit host_vars/arch-vps/vault.yml # Homelab secrets
# 4. Deploy complete infrastructure # 4. Deploy to specific environment
ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass ansible-playbook playbooks/homelab.yml # Deploy homelab
ansible-playbook playbooks/production.yml # Deploy production
ansible-playbook site.yml # Deploy both
``` ```
**Total deployment time**: 8-14 minutes for complete stack **Deployment times**:
- Homelab (full stack): 8-14 minutes
- Production (minimal): 3-5 minutes
### Verify Deployment ### Verify Deployment
```bash ```bash
# Check services # Check homelab services
curl -I https://auth.jnss.me/ # Authentik SSO curl -I https://auth.jnss.me/ # Authentik SSO
curl -I https://git.jnss.me/ # Gitea (if enabled) curl -I https://cloud.jnss.me/ # Nextcloud
ansible homelab -a "systemctl status postgresql valkey caddy"
# Check infrastructure # Check production services
ansible arch-vps -m command -a "systemctl status postgresql valkey caddy" curl -I https://sigvild.no/ # Sigvild Gallery
ansible production -a "systemctl status sigvild-gallery caddy"
``` ```
## Key Features ## Key Features
@@ -116,21 +137,32 @@ ansible arch-vps -m command -a "systemctl status postgresql valkey caddy"
## Core Services ## Core Services
### Infrastructure Services (Native systemd) ### Homelab Services (arch-vps)
**Infrastructure (Native systemd)**:
- **PostgreSQL** - High-performance database with Unix socket support - **PostgreSQL** - High-performance database with Unix socket support
- **Valkey** - Redis-compatible cache with Unix socket support - **Valkey** - Redis-compatible cache with Unix socket support
- **Caddy** - Automatic HTTPS reverse proxy with Cloudflare DNS - **Caddy** - Automatic HTTPS reverse proxy with Cloudflare DNS
- **Podman** - Rootless container runtime with systemd integration - **Podman** - Rootless container runtime with systemd integration
### Authentication Services **Authentication**:
- **Authentik** - Modern SSO server with OAuth2/OIDC/SAML support - **Authentik** - Modern SSO server with OAuth2/OIDC/SAML support
- **Forward Auth** - Transparent service protection via Caddy integration - **Forward Auth** - Transparent service protection via Caddy
- **Multi-Factor Authentication** - TOTP, WebAuthn, SMS support
### Application Services (Containerized) **Applications (Containerized)**:
- **Nextcloud** - Personal cloud storage and file sync
- **Gitea** - Self-hosted Git service with SSO integration - **Gitea** - Self-hosted Git service with SSO integration
- **Gallery** - Media gallery with authentication
- **Custom Services** - Template for additional service integration ### Production Services (mini-vps)
**Infrastructure**:
- **Caddy** - Automatic HTTPS reverse proxy with Cloudflare DNS
**Applications**:
- **Sigvild Gallery** - Wedding photo gallery with PocketBase API
- Frontend: SvelteKit static site
- Backend: Go + SQLite (PocketBase)
- Domains: sigvild.no, api.sigvild.no
## Architecture Benefits ## Architecture Benefits

View File

@@ -1,68 +0,0 @@
---
# Deploy Unix Socket Updates for PostgreSQL, Valkey, Authentik, and Gitea
# This playbook updates services to use Unix sockets for inter-process communication
- name: Deploy Unix socket configuration updates
hosts: arch-vps
become: yes
tasks:
- name: Display deployment plan
debug:
msg: |
🔧 Unix Socket Migration Plan
=============================
📦 Services to Update:
1. PostgreSQL - Switch to socket-only (no TCP)
2. Valkey - Add Unix socket support
3. Authentik - Use sockets for DB/cache
4. Gitea - Use sockets for DB/cache
🔒 Security Benefits:
- Zero network exposure for databases
- Better performance (25-30% faster)
- Simplified security model
- name: Update PostgreSQL to socket-only
include_role:
name: postgresql
tags: [postgresql]
- name: Update Valkey with Unix socket
include_role:
name: valkey
tags: [valkey]
- name: Update Authentik for Unix sockets
include_role:
name: authentik
tags: [authentik]
- name: Update Gitea for Unix sockets
include_role:
name: gitea
tags: [gitea]
- name: Verify socket files exist
stat:
path: "{{ item }}"
loop:
- /run/postgresql/.s.PGSQL.5432
- /run/valkey/valkey.sock
register: socket_checks
- name: Display results
debug:
msg: |
✅ Deployment Complete!
Socket Status:
{% for check in socket_checks.results %}
- {{ check.item }}: {{ "EXISTS" if check.stat.exists else "MISSING" }}
{% endfor %}
Next Steps:
1. Check service logs: journalctl -u authentik-pod
2. Test Authentik: curl http://arch-vps:9000/if/flow/initial-setup/
3. Test Gitea: curl http://arch-vps:3000/

View File

@@ -1,139 +1,9 @@
# Architecture Decision Records (ADR) # Architecture Decision Records (ADR)
This document records the significant architectural decisions made in the rick-infra project, particularly focusing on the authentication and infrastructure components. This document records the significant architectural decisions made in the rick-infra project.
## Table of Contents
- [ADR-001: Native Database Services over Containerized](#adr-001-native-database-services-over-containerized)
- [ADR-002: Unix Socket IPC Architecture](#adr-002-unix-socket-ipc-architecture)
- [ADR-003: Podman + systemd Container Orchestration](#adr-003-podman--systemd-container-orchestration)
- [ADR-004: Forward Authentication Security Model](#adr-004-forward-authentication-security-model)
- [ADR-005: Rootful Containers with Infrastructure Fact Pattern](#adr-005-rootful-containers-with-infrastructure-fact-pattern)
--- ---
## Unix Socket IPC Architecture
## ADR-001: Native Database Services over Containerized
**Status**: ✅ Accepted
**Date**: December 2025
**Deciders**: Infrastructure Team
**Technical Story**: Need reliable database and cache services for containerized applications with optimal performance and security.
### Context
When deploying containerized applications that require database and cache services, there are two primary architectural approaches:
1. **Containerized Everything**: Deploy databases and cache services as containers
2. **Native Infrastructure Services**: Use systemd-managed native services for infrastructure, containers for applications
### Decision
We will use **native systemd services** for core infrastructure components (PostgreSQL, Valkey/Redis) while using containers only for application services (Authentik, Gitea, etc.).
### Rationale
#### Performance Benefits
- **No Container Overhead**: Native services eliminate container runtime overhead
```bash
# Native PostgreSQL: Direct filesystem access
# Containerized PostgreSQL: Container filesystem layer overhead
```
- **Direct System Resources**: Native services access system resources without abstraction layers
- **Optimized Memory Management**: OS-level memory management without container constraints
- **Disk I/O Performance**: Direct access to storage without container volume mounting overhead
#### Security Advantages
- **Unix Socket Security**: Native services can provide Unix sockets with filesystem-based security
```bash
# Native: /var/run/postgresql/.s.PGSQL.5432 (postgres:postgres 0770)
# Containerized: Requires network exposure or complex socket mounting
```
- **Reduced Attack Surface**: No container runtime vulnerabilities for critical infrastructure
- **OS-Level Security**: Standard system security mechanisms apply directly
- **Group-Based Access Control**: Simple Unix group membership for service access
#### Operational Excellence
- **Standard Tooling**: Familiar systemd service management
```bash
systemctl status postgresql
journalctl -u postgresql -f
systemctl restart postgresql
```
- **Package Management**: Standard OS package updates and security patches
- **Backup Integration**: Native backup tools work seamlessly
```bash
pg_dump -h /var/run/postgresql authentik > backup.sql
```
- **Monitoring**: Standard system monitoring tools apply directly
#### Reliability
- **systemd Integration**: Robust service lifecycle management
```ini
[Unit]
Description=PostgreSQL database server
After=network.target
[Service]
Type=forking
Restart=always
RestartSec=5
```
- **Resource Isolation**: systemd provides resource isolation without container overhead
- **Proven Architecture**: Battle-tested approach used by major infrastructure providers
### Consequences
#### Positive
- **Performance**: 15-25% better database performance in benchmarks
- **Security**: Eliminated network-based database attacks via Unix sockets
- **Operations**: Simplified backup, monitoring, and maintenance procedures
- **Resource Usage**: Lower memory and CPU overhead
- **Reliability**: More predictable service behavior
#### Negative
- **Containerization Purity**: Not a "pure" containerized environment
- **Portability**: Slightly less portable than full-container approach
- **Learning Curve**: Team needs to understand both systemd and container management
#### Neutral
- **Complexity**: Different but not necessarily more complex than container orchestration
- **Tooling**: Different toolset but equally capable
### Implementation Notes
```yaml
# Infrastructure services (native systemd)
- postgresql # Native database service
- valkey # Native cache service
- caddy # Native reverse proxy
- podman # Container runtime
# Application services (containerized)
- authentik # Authentication service
- gitea # Git service
```
### Alternatives Considered
1. **Full Containerization**: Rejected due to performance and operational complexity
2. **Mixed with Docker**: Rejected in favor of Podman for security benefits
3. **VM-based Infrastructure**: Rejected due to resource overhead
---
## ADR-002: Unix Socket IPC Architecture
**Status**: ✅ Accepted
**Date**: December 2025
**Deciders**: Infrastructure Team
**Technical Story**: Secure and performant communication between containerized applications and native infrastructure services.
### Context ### Context
@@ -141,11 +11,10 @@ Containerized applications need to communicate with database and cache services.
1. **Network TCP/IP**: Standard network protocols 1. **Network TCP/IP**: Standard network protocols
2. **Unix Domain Sockets**: Filesystem-based IPC 2. **Unix Domain Sockets**: Filesystem-based IPC
3. **Shared Memory**: Direct memory sharing (complex)
### Decision ### Decision
We will use **Unix domain sockets** for all communication between containerized applications and infrastructure services. We will use **Unix domain sockets** for all communication between applications and infrastructure services.
### Rationale ### Rationale
@@ -269,10 +138,6 @@ podman exec authentik-server id
## ADR-003: Podman + systemd Container Orchestration ## ADR-003: Podman + systemd Container Orchestration
**Status**: ✅ Accepted
**Date**: December 2025
**Updated**: December 2025 (System-level deployment pattern)
**Deciders**: Infrastructure Team
**Technical Story**: Container orchestration solution for secure application deployment with systemd integration. **Technical Story**: Container orchestration solution for secure application deployment with systemd integration.
### Context ### Context
@@ -493,12 +358,9 @@ ps aux | grep authentik-server | head -1 | awk '{print $2}' | \
--- ---
## ADR-004: Forward Authentication Security Model ## OAuth/OIDC and Forward Authentication Security Model
**Status**: ✅ Accepted **Technical Story**: Centralized authentication and authorization for multiple services using industry-standard OAuth2/OIDC protocols where supported, with forward authentication as a fallback.
**Date**: December 2025
**Deciders**: Infrastructure Team
**Technical Story**: Centralized authentication and authorization for multiple services without modifying existing applications.
### Context ### Context
@@ -506,53 +368,122 @@ Authentication strategies for multiple services:
1. **Per-Service Authentication**: Each service handles its own authentication 1. **Per-Service Authentication**: Each service handles its own authentication
2. **Shared Database**: Services share authentication database 2. **Shared Database**: Services share authentication database
3. **Forward Authentication**: Reverse proxy handles authentication 3. **OAuth2/OIDC Integration**: Services implement standard OAuth2/OIDC clients
4. **OAuth2/OIDC Integration**: Services implement OAuth2 clients 4. **Forward Authentication**: Reverse proxy handles authentication for services without OAuth support
### Decision ### Decision
We will use **forward authentication** with Caddy reverse proxy and Authentik authentication server as the primary authentication model. We will use **OAuth2/OIDC integration** as the primary authentication method for services that support it, and **forward authentication** for services that do not support native OAuth2/OIDC integration.
### Rationale ### Rationale
#### Security Benefits #### OAuth/OIDC as Primary Method
- **Single Point of Control**: Centralized authentication policy **Security Benefits**:
- **Standard Protocol**: Industry-standard authentication flow (RFC 6749, RFC 7636)
- **Token-Based Security**: Secure JWT tokens with cryptographic signatures
- **Proper Session Management**: Native application session handling with refresh tokens
- **Scope-Based Authorization**: Fine-grained permission control via OAuth scopes
- **PKCE Support**: Protection against authorization code interception attacks
**Integration Benefits**:
- **Native Support**: Applications designed for OAuth/OIDC work seamlessly
- **Better UX**: Proper redirect flows, logout handling, and token refresh
- **API Access**: OAuth tokens enable secure API integrations
- **Standard Claims**: OpenID Connect user info endpoint provides standardized user data
- **Multi-Application SSO**: Proper single sign-on with token sharing
**Examples**: Nextcloud, Gitea, Grafana, many modern applications
#### Forward Auth as Fallback
**Use Cases**:
- Services without OAuth/OIDC support
- Legacy applications that cannot be modified
- Static sites requiring authentication
- Simple internal tools
**Security Benefits**:
- **Zero Application Changes**: Protect existing services without modification - **Zero Application Changes**: Protect existing services without modification
- **Consistent Security**: Same security model across all services - **Header-Based Identity**: Simple identity propagation to backend
- **Session Management**: Centralized session handling and timeouts - **Transparent Protection**: Services receive pre-authenticated requests
- **Multi-Factor Authentication**: MFA applied consistently across services
#### Operational Advantages **Limitations**:
- **Non-Standard**: Not using industry-standard authentication protocols
- **Proxy Dependency**: All requests must flow through authenticating proxy
- **Limited Logout**: Complex logout scenarios across services
- **Header Trust**: Backend must trust proxy-provided headers
- **Simplified Deployment**: No per-service authentication setup #### Shared Benefits (Both Methods)
- **Single Point of Control**: Centralized authentication policy via Authentik
- **Consistent Security**: Same authentication provider across all services
- **Multi-Factor Authentication**: MFA applied consistently via Authentik
- **Audit Trail**: Centralized authentication logging - **Audit Trail**: Centralized authentication logging
- **Policy Management**: Single place to manage access policies
- **User Management**: One system for all user administration - **User Management**: One system for all user administration
- **Service Independence**: Services focus on business logic
#### Integration Benefits
- **Transparent to Applications**: Services receive authenticated requests
- **Header-Based Identity**: Simple identity propagation
```http
Remote-User: john.doe
Remote-Name: John Doe
Remote-Email: john.doe@company.com
Remote-Groups: admins,developers
```
- **Gradual Migration**: Can protect services incrementally
- **Fallback Support**: Can coexist with service-native authentication
### Implementation Architecture ### Implementation Architecture
#### OAuth/OIDC Flow (Primary Method)
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ │ Service │ │ Authentik │
│ │ │ (OAuth App) │ │ (IdP) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ Access Service │ │
│─────────────────▶│ │
│ │ │
│ │ No session │
│ 302 → OAuth │ │
│◀─────────────────│ │
│ │ │
│ GET /authorize?client_id=...&redirect_uri=...
│──────────────────────────────────────▶│
│ │ │
│ Login form (if not authenticated) │
│◀────────────────────────────────────│
│ │ │
│ Credentials │ │
│─────────────────────────────────────▶│
│ │ │
│ 302 → callback?code=AUTH_CODE │
│◀────────────────────────────────────│
│ │ │
│ GET /callback?code=AUTH_CODE │
│─────────────────▶│ │
│ │ │
│ │ POST /token │
│ │ code=AUTH_CODE │
│ │─────────────────▶│
│ │ │
│ │ access_token │
│ │ id_token (JWT) │
│ │◀─────────────────│
│ │ │
│ Set-Cookie │ GET /userinfo │
│ 302 → /dashboard │─────────────────▶│
│◀─────────────────│ │
│ │ User claims │
│ │◀─────────────────│
│ │ │
│ GET /dashboard │ │
│─────────────────▶│ │
│ │ │
│ Dashboard │ │
│◀─────────────────│ │
```
#### Forward Auth Flow (Fallback Method)
``` ```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ │ Caddy │ │ Authentik │ │ Service │ │ User │ │ Caddy │ │ Authentik │ │ Service │
│ │ │ (Proxy) │ │ (Auth) │ │ (Backend) │ │ │ │ (Proxy) │ │ (Forward) │ │ (Backend) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │ │ │ │ │
│ GET /dashboard │ │ │ │ GET / │ │ │
│─────────────────▶│ │ │ │─────────────────▶│ │ │
│ │ │ │ │ │ │ │
│ │ Forward Auth │ │ │ │ Forward Auth │ │
@@ -561,19 +492,19 @@ We will use **forward authentication** with Caddy reverse proxy and Authentik au
│ │ 401 Unauthorized │ │ │ │ 401 Unauthorized │ │
│ │◀─────────────────│ │ │ │◀─────────────────│ │
│ │ │ │ │ │ │ │
│ 302 → /auth/login│ │ │ │ 302 → /auth │ │ │
│◀─────────────────│ │ │ │◀─────────────────│ │ │
│ │ │ │ │ │ │ │
│ Login form │ │ │ │ Login form │ │ │
│─────────────────▶│─────────────────▶│ │ │──────────────────────────────────────▶│ │
│ │ │ │ │ │ │ │
│ Credentials │ │ │ │ Credentials │ │ │
│─────────────────▶│─────────────────▶│ │ │──────────────────────────────────────▶│ │
│ │ │ │ │ │ │ │
│ Set-Cookie │ │ │ │ Set-Cookie │ │ │
│◀─────────────────│◀─────────────────│ │ │◀──────────────────────────────────────│ │
│ │ │ │ │ │ │ │
│ GET /dashboard │ │ │ │ GET / │ │ │
│─────────────────▶│ │ │ │─────────────────▶│ │ │
│ │ │ │ │ │ │ │
│ │ Forward Auth │ │ │ │ Forward Auth │ │
@@ -582,21 +513,93 @@ We will use **forward authentication** with Caddy reverse proxy and Authentik au
│ │ 200 + Headers │ │ │ │ 200 + Headers │ │
│ │◀─────────────────│ │ │ │◀─────────────────│ │
│ │ │ │ │ │ │ │
│ │ GET /dashboard + Auth Headers │ │ Proxy + Headers │
│ │─────────────────────────────────────▶│ │ │─────────────────────────────────────▶│
│ │ │ │
│ │ Dashboard Content │ │ Response │
│ │◀─────────────────────────────────────│ │ │◀─────────────────────────────────────│
│ │ │ │
Dashboard Content
│◀─────────────────│ │◀─────────────────│
``` ```
### Caddy Configuration ### OAuth/OIDC Configuration Examples
#### Nextcloud OAuth Configuration
```php
// Nextcloud config.php
'oidc_login_provider_url' => 'https://auth.jnss.me/application/o/nextcloud/',
'oidc_login_client_id' => 'nextcloud-client-id',
'oidc_login_client_secret' => 'secret-from-authentik',
'oidc_login_auto_redirect' => true,
'oidc_login_end_session_redirect' => true,
'oidc_login_button_text' => 'Login with SSO',
'oidc_login_hide_password_form' => true,
'oidc_login_use_id_token' => true,
'oidc_login_attributes' => [
'id' => 'preferred_username',
'name' => 'name',
'mail' => 'email',
'groups' => 'groups',
],
'oidc_login_default_group' => 'users',
'oidc_login_use_external_storage' => false,
'oidc_login_scope' => 'openid profile email groups',
'oidc_login_proxy_ldap' => false,
'oidc_login_disable_registration' => false,
'oidc_login_redir_fallback' => true,
'oidc_login_tls_verify' => true,
```
#### Gitea OAuth Configuration
```ini
# Gitea app.ini
[openid]
ENABLE_OPENID_SIGNIN = false
ENABLE_OPENID_SIGNUP = false
[oauth2_client]
REGISTER_EMAIL_CONFIRM = false
OPENID_CONNECT_SCOPES = openid email profile groups
ENABLE_AUTO_REGISTRATION = true
USERNAME = preferred_username
EMAIL = email
ACCOUNT_LINKING = auto
```
**Authentik Provider Configuration** (Gitea):
- Provider Type: OAuth2/OpenID Provider
- Client ID: `gitea`
- Client Secret: Generated by Authentik
- Redirect URIs: `https://git.jnss.me/user/oauth2/Authentik/callback`
- Scopes: `openid`, `profile`, `email`, `groups`
#### Authentik OAuth2 Provider Settings
```yaml
# OAuth2/OIDC Provider configuration in Authentik
name: "Nextcloud OAuth Provider"
authorization_flow: "default-authorization-flow"
client_type: "confidential"
client_id: "nextcloud-client-id"
redirect_uris: "https://cloud.jnss.me/apps/oidc_login/oidc"
signing_key: "authentik-default-key"
property_mappings:
- "authentik default OAuth Mapping: OpenID 'openid'"
- "authentik default OAuth Mapping: OpenID 'email'"
- "authentik default OAuth Mapping: OpenID 'profile'"
- "Custom: Groups" # Maps user groups to 'groups' claim
```
### Forward Auth Configuration Examples
#### Caddy Configuration for Forward Auth
```caddyfile ```caddyfile
# Service protection template # whoami service with forward authentication
dashboard.jnss.me { whoami.jnss.me {
# Forward authentication to Authentik # Forward authentication to Authentik
forward_auth https://auth.jnss.me { forward_auth https://auth.jnss.me {
uri /outpost.goauthentik.io/auth/caddy uri /outpost.goauthentik.io/auth/caddy
@@ -608,120 +611,194 @@ dashboard.jnss.me {
} }
``` ```
### Service Integration #### Authentik Proxy Provider Configuration
```yaml
# Authentik Proxy Provider for forward auth
name: "Whoami Forward Auth"
type: "proxy"
authorization_flow: "default-authorization-flow"
external_host: "https://whoami.jnss.me"
internal_host: "http://localhost:8080"
skip_path_regex: "^/(health|metrics).*"
mode: "forward_single" # Single application mode
```
#### Service Integration (Forward Auth)
Services receive authentication information via HTTP headers: Services receive authentication information via HTTP headers:
```python ```python
# Example service code (Python Flask) # Example service code (Python Flask)
@app.route('/dashboard') @app.route('/')
def dashboard(): def index():
username = request.headers.get('Remote-User') username = request.headers.get('Remote-User')
name = request.headers.get('Remote-Name') name = request.headers.get('Remote-Name')
email = request.headers.get('Remote-Email') email = request.headers.get('Remote-Email')
groups = request.headers.get('Remote-Groups', '').split(',') groups = request.headers.get('Remote-Groups', '').split(',')
if 'admins' in groups: return render_template('index.html',
# Admin functionality
pass
return render_template('dashboard.html',
username=username, username=username,
name=name) name=name,
``` email=email,
groups=groups)
### Authentik Provider Configuration
```yaml
# Authentik Proxy Provider configuration
name: "Service Forward Auth"
authorization_flow: "default-authorization-flow"
external_host: "https://service.jnss.me"
internal_host: "http://localhost:8080"
skip_path_regex: "^/(health|metrics|static).*"
``` ```
### Authorization Policies ### Authorization Policies
Both OAuth and Forward Auth support Authentik authorization policies:
```yaml ```yaml
# Example authorization policy in Authentik # Example authorization policy in Authentik
policy_bindings: policy_bindings:
- policy: "group_admins_only" - policy: "group_admins_only"
target: "service_dashboard" target: "nextcloud_oauth_provider"
order: 0 order: 0
- policy: "deny_external_ips" - policy: "require_mfa"
target: "admin_endpoints" target: "gitea_oauth_provider"
order: 1 order: 1
- policy: "internal_network_only"
target: "whoami_proxy_provider"
order: 0
``` ```
### Decision Matrix: OAuth/OIDC vs Forward Auth
| Criteria | OAuth/OIDC | Forward Auth |
|----------|-----------|-------------|
| **Application Support** | Requires native OAuth/OIDC support | Any application |
| **Protocol Standard** | Industry standard (RFC 6749, 7636) | Proprietary/custom |
| **Token Management** | Native refresh tokens, proper expiry | Session-based only |
| **Logout Handling** | Proper logout flow | Complex, proxy-dependent |
| **API Access** | Full API support via tokens | Header-only |
| **Implementation Effort** | Configure OAuth settings | Zero app changes |
| **User Experience** | Standard OAuth redirects | Transparent |
| **Security Model** | Token-based with scopes | Header trust model |
| **When to Use** | **Nextcloud, Gitea, modern apps** | **Static sites, legacy apps, whoami** |
### Consequences ### Consequences
#### Positive #### Positive
- **Security**: Consistent, centralized authentication and authorization - **Standards Compliance**: OAuth/OIDC uses industry-standard protocols
- **Simplicity**: No application changes required for protection - **Security**: Multiple authentication options with appropriate security models
- **Flexibility**: Fine-grained access control through Authentik policies - **Flexibility**: Right tool for each service (OAuth when possible, forward auth when needed)
- **Auditability**: Centralized authentication logging - **Auditability**: Centralized authentication logging via Authentik
- **User Experience**: Single sign-on across all services - **User Experience**: Proper SSO across all services
- **Token Security**: OAuth provides secure token refresh and scope management
- **Graceful Degradation**: Forward auth available for services without OAuth support
#### Negative #### Negative
- **Single Point of Failure**: Authentication system failure affects all services - **Complexity**: Need to understand two authentication methods
- **Performance**: Additional hop for authentication checks - **Configuration Overhead**: OAuth requires per-service configuration
- **Complexity**: Additional component in the request path - **Single Point of Failure**: Authentik failure affects all services
- **Learning Curve**: Team must understand OAuth flows and forward auth model
#### Mitigation Strategies #### Mitigation Strategies
- **High Availability**: Robust deployment and monitoring of auth components - **Documentation**: Clear decision guide for choosing OAuth vs forward auth
- **Caching**: Session caching to reduce authentication overhead - **Templates**: Reusable OAuth configuration templates for common services
- **Fallback**: Emergency bypass procedures for critical services - **High Availability**: Robust deployment and monitoring of Authentik
- **Monitoring**: Comprehensive monitoring of authentication flow - **Monitoring**: Comprehensive monitoring of both authentication flows
- **Testing**: Automated tests for authentication flows
### Security Considerations ### Security Considerations
#### Session Security #### OAuth/OIDC Security
```yaml ```yaml
# Authentik session settings # Authentik OAuth2 Provider security settings
session_cookie_age: 3600 # 1 hour authorization_code_validity: 60 # 1 minute
session_cookie_secure: true access_code_validity: 3600 # 1 hour
session_cookie_samesite: "Strict" refresh_code_validity: 2592000 # 30 days
session_remember_me: false include_claims_in_id_token: true
signing_key: "authentik-default-key"
sub_mode: "hashed_user_id"
issuer_mode: "per_provider"
``` ```
**Best Practices**:
- Use PKCE for all OAuth flows (protection against interception)
- Implement proper token rotation (refresh tokens expire and rotate)
- Validate `aud` (audience) and `iss` (issuer) claims in JWT tokens
- Use short-lived access tokens (1 hour)
- Store client secrets securely (Ansible Vault)
#### Forward Auth Security
```yaml
# Authentik Proxy Provider security settings
token_validity: 3600 # 1 hour session
cookie_domain: ".jnss.me"
skip_path_regex: "^/(health|metrics|static).*"
```
**Best Practices**:
- Trust only Authentik-provided headers
- Validate `Remote-User` header exists before granting access
- Use HTTPS for all forward auth endpoints
- Implement proper session timeouts
- Strip user-provided authentication headers at proxy
#### Access Control #### Access Control
- **Group-Based Authorization**: Users assigned to groups, groups to applications - **Group-Based Authorization**: Users assigned to groups, groups to applications
- **Time-Based Access**: Temporary access grants - **Policy Engine**: Authentik policies for fine-grained access control
- **IP-Based Restrictions**: Geographic or network-based access control
- **MFA Requirements**: Multi-factor authentication for sensitive services - **MFA Requirements**: Multi-factor authentication for sensitive services
- **IP-Based Restrictions**: Geographic or network-based access control
- **Time-Based Access**: Temporary access grants via policies
#### Audit Logging #### Audit Logging
```json ```json
{ {
"timestamp": "2025-12-11T17:52:31Z", "timestamp": "2025-12-15T10:30:00Z",
"event": "authentication_success", "event": "oauth_authorization",
"user": "john.doe", "user": "john.doe",
"service": "dashboard.jnss.me", "application": "nextcloud",
"scopes": ["openid", "email", "profile", "groups"],
"ip": "192.168.1.100", "ip": "192.168.1.100",
"user_agent": "Mozilla/5.0..." "user_agent": "Mozilla/5.0..."
} }
``` ```
### Alternative Models Supported ### Implementation Examples by Service Type
While forward auth is primary, we also support: #### OAuth/OIDC Services (Primary Method)
1. **OAuth2/OIDC Integration**: For applications that can implement OAuth2 **Nextcloud**:
2. **API Key Authentication**: For service-to-service communication ```caddyfile
3. **Service-Native Auth**: For legacy applications that cannot be easily protected cloud.jnss.me {
reverse_proxy localhost:8080
}
# OAuth configured within Nextcloud application
```
### Implementation Examples **Gitea**:
```caddyfile
git.jnss.me {
reverse_proxy localhost:3000
}
# OAuth configured within Gitea application settings
```
#### Protecting a Static Site #### Forward Auth Services (Fallback Method)
**Whoami (test/demo service)**:
```caddyfile
whoami.jnss.me {
forward_auth https://auth.jnss.me {
uri /outpost.goauthentik.io/auth/caddy
copy_headers Remote-User Remote-Name Remote-Email Remote-Groups
}
reverse_proxy localhost:8080
}
```
**Static Documentation Site**:
```caddyfile ```caddyfile
docs.jnss.me { docs.jnss.me {
forward_auth https://auth.jnss.me { forward_auth https://auth.jnss.me {
@@ -734,33 +811,44 @@ docs.jnss.me {
} }
``` ```
#### Protecting an API **Internal API (no OAuth support)**:
```caddyfile ```caddyfile
api.jnss.me { api.jnss.me {
forward_auth https://auth.jnss.me { forward_auth https://auth.jnss.me {
uri /outpost.goauthentik.io/auth/caddy uri /outpost.goauthentik.io/auth/caddy
copy_headers Remote-User Remote-Email Remote-Groups copy_headers Remote-User Remote-Email Remote-Groups
} }
reverse_proxy localhost:3000 reverse_proxy localhost:3000
} }
``` ```
#### Public Endpoints with Selective Protection #### Selective Protection (Public + Protected Paths)
```caddyfile ```caddyfile
app.jnss.me { app.jnss.me {
# Public endpoints (no auth) # Public endpoints (no auth required)
handle /health { handle /health {
reverse_proxy localhost:8080 reverse_proxy localhost:8080
} }
handle /metrics {
reverse_proxy localhost:8080
}
handle /public/* { handle /public/* {
reverse_proxy localhost:8080 reverse_proxy localhost:8080
} }
# Protected endpoints # Protected endpoints (forward auth)
handle /admin/* {
forward_auth https://auth.jnss.me {
uri /outpost.goauthentik.io/auth/caddy
copy_headers Remote-User Remote-Groups
}
reverse_proxy localhost:8080
}
# Default: protected
handle { handle {
forward_auth https://auth.jnss.me { forward_auth https://auth.jnss.me {
uri /outpost.goauthentik.io/auth/caddy uri /outpost.goauthentik.io/auth/caddy
@@ -773,18 +861,17 @@ app.jnss.me {
### Alternatives Considered ### Alternatives Considered
1. **OAuth2 Only**: Rejected due to application modification requirements 1. **OAuth2/OIDC Only**: Rejected because many services don't support OAuth natively
2. **Shared Database**: Rejected due to tight coupling between services 2. **Forward Auth Only**: Rejected because it doesn't leverage native OAuth support in modern apps
3. **VPN-Based Access**: Rejected due to operational complexity for web services 3. **Per-Service Authentication**: Rejected due to management overhead and inconsistent security
4. **Per-Service Authentication**: Rejected due to management overhead 4. **Shared Database**: Rejected due to tight coupling between services
5. **VPN-Based Access**: Rejected due to operational complexity for web services
6. **SAML**: Rejected in favor of modern OAuth2/OIDC standards
--- ---
## ADR-005: Rootful Containers with Infrastructure Fact Pattern ## Rootful Containers with Infrastructure Fact Pattern
**Status**: ✅ Accepted
**Date**: December 2025
**Deciders**: Infrastructure Team
**Technical Story**: Enable containerized applications to access native infrastructure services (PostgreSQL, Valkey) via Unix sockets with group-based permissions. **Technical Story**: Enable containerized applications to access native infrastructure services (PostgreSQL, Valkey) via Unix sockets with group-based permissions.
### Context ### Context
@@ -1038,25 +1125,122 @@ curl -I http://127.0.0.1:9000/
1. **Rootless with user namespace** - Discarded due to GID remapping breaking group-based socket access 1. **Rootless with user namespace** - Discarded due to GID remapping breaking group-based socket access
2. **TCP-only connections** - Rejected to maintain Unix socket security and performance benefits 2. **TCP-only connections** - Rejected to maintain Unix socket security and performance benefits
3. **Hardcoded GIDs** - Rejected for portability; facts provide dynamic resolution 3. **Hardcoded GIDs** - Rejected for portability; facts provide dynamic resolution
4. **Directory permissions (777)** - Rejected for security; group-based access more restrictive 4. **Directory permissions (777)** - Rejected for security; group-based access more restrictive. This is then later changed again to 777, due to Nextcloud switching from root to www-data, breaking group-based permissions.
--- ---
## Summary ---
## ADR-007: Multi-Environment Infrastructure Architecture
These architecture decisions collectively create a robust, secure, and performant infrastructure: **Date**: December 2025
**Status**: Accepted
**Context**: Separation of homelab services from production client projects
- **Native Services** provide optimal performance and security ### Decision
- **Unix Sockets** eliminate network attack vectors
- **Podman + systemd** delivers secure container orchestration
- **Forward Authentication** enables centralized security without application changes
- **Rootful Container Pattern** enables group-based socket access with infrastructure fact sharing
The combination results in an infrastructure that prioritizes security and performance while maintaining operational simplicity and reliability. Rick-infra will manage two separate environments with different purposes and uptime requirements:
## References 1. **Homelab Environment** (arch-vps)
- Purpose: Personal services and experimentation
- Infrastructure: Full stack (PostgreSQL, Valkey, Podman, Caddy)
- Services: Authentik, Nextcloud, Gitea
- Uptime requirement: Best effort
- [Service Integration Guide](service-integration-guide.md) 2. **Production Environment** (mini-vps)
- [Authentik Deployment Guide](authentik-deployment-guide.md) - Purpose: Client projects requiring high uptime
- [Security Hardening](security-hardening.md) - Infrastructure: Minimal (Caddy only)
- [Authentik Role Documentation](../roles/authentik/README.md) - Services: Sigvild Gallery
- Uptime requirement: High availability
### Rationale
**Separation of Concerns**:
- Personal experiments don't affect client services
- Client services isolated from homelab maintenance
- Clear distinction between environments in code
**Infrastructure Optimization**:
- Production runs minimal services (no PostgreSQL/Valkey overhead)
- Homelab can be rebooted/upgraded without affecting clients
- Cost optimization: smaller VPS for production
**Operational Flexibility**:
- Different backup strategies per environment
- Different monitoring/alerting levels
- Independent deployment schedules
### Implementation
**Variable Organization**:
```
rick-infra/
├── group_vars/
│ └── production/ # Production environment config
│ ├── main.yml
│ └── vault.yml
├── host_vars/
│ └── arch-vps/ # Homelab host config
│ ├── main.yml
│ └── vault.yml
└── playbooks/
├── homelab.yml # Homelab deployment
├── production.yml # Production deployment
└── site.yml # Orchestrates both
```
**Playbook Structure**:
- `site.yml` imports both homelab.yml and production.yml
- Each playbook manually loads variables (Ansible 2.20 workaround)
- Services deploy only to their designated environment
**Inventory Groups**:
```yaml
homelab:
hosts:
arch-vps:
ansible_host: 69.62.119.31
production:
hosts:
mini-vps:
ansible_host: 72.62.91.251
```
### Migration Example
**Sigvild Gallery Migration** (December 2025):
- **From**: arch-vps (homelab)
- **To**: mini-vps (production)
- **Reason**: Client project requiring higher uptime
- **Process**:
1. Created backup on arch-vps
2. Deployed to mini-vps with automatic restore
3. Updated DNS (5 min downtime)
4. Removed from arch-vps configuration
### Consequences
**Positive**:
- Clear separation of personal vs. client services
- Reduced blast radius for experiments
- Optimized resource usage per environment
- Independent scaling and management
**Negative**:
- Increased complexity in playbook organization
- Need to manage multiple VPS instances
- Ansible 2.20 variable loading requires workarounds
- Duplicate infrastructure code (Caddy on both)
**Neutral**:
- Services can be migrated between environments with minimal friction
- Backup/restore procedures work across environments
- Group_vars vs. host_vars hybrid approach
### Future Considerations
- Consider grouping multiple client projects on production VPS
- Evaluate if homelab needs full infrastructure stack
- Monitor for opportunities to share infrastructure between environments
- Document migration procedures for moving services between environments
---

View File

@@ -7,7 +7,8 @@ This document describes the comprehensive authentication and authorization strat
Rick-infra implements a modern, security-focused authentication architecture that provides: Rick-infra implements a modern, security-focused authentication architecture that provides:
- **Centralized SSO**: Single sign-on across all services via Authentik - **Centralized SSO**: Single sign-on across all services via Authentik
- **Forward Authentication**: Transparent protection without application changes - **OAuth2/OIDC Integration**: Industry-standard authentication for services that support it (primary method)
- **Forward Authentication**: Transparent protection for legacy applications and services without OAuth support (fallback method)
- **Zero Network Exposure**: Database and cache communication via Unix sockets - **Zero Network Exposure**: Database and cache communication via Unix sockets
- **Granular Authorization**: Fine-grained access control through groups and policies - **Granular Authorization**: Fine-grained access control through groups and policies
- **Standards Compliance**: OAuth2, OIDC, SAML support for enterprise integration - **Standards Compliance**: OAuth2, OIDC, SAML support for enterprise integration
@@ -98,15 +99,86 @@ sequenceDiagram
## Service Integration Patterns ## Service Integration Patterns
### Pattern 1: Forward Authentication (Recommended) ### Choosing the Right Pattern
**Use Case**: Existing HTTP services that don't need to handle authentication **Use OAuth2/OIDC (Primary Method)** when:
- ✅ Your service/application natively supports OAuth2/OIDC
- ✅ Examples: Nextcloud, Gitea, Grafana, modern web applications
- ✅ Provides better security, standard protocols, and proper token management
**Use Forward Authentication (Fallback Method)** when:
- ⚠️ Service doesn't support OAuth2/OIDC integration
- ⚠️ Legacy applications that cannot be modified
- ⚠️ Static sites requiring authentication
- ⚠️ Simple internal tools without authentication capabilities
---
### Pattern 1: OAuth2/OIDC Integration (Primary Method)
**Use Case**: Applications with native OAuth2/OIDC support (Nextcloud, Gitea, Grafana, etc.)
**Benefits**: **Benefits**:
- No application code changes required - ✅ Industry-standard authentication protocol (RFC 6749, RFC 7636)
- Consistent authentication across all services - ✅ Secure token-based authentication with JWT
- Service receives authenticated user information via headers - ✅ Proper session management with refresh tokens
- Centralized session management - ✅ Native application integration
- ✅ Support for API access tokens
- ✅ Better logout handling and token refresh
**Implementation**:
```python
# Example OAuth2 configuration
OAUTH2_CONFIG = {
'client_id': 'your-client-id',
'client_secret': 'your-client-secret',
'server_metadata_url': 'https://auth.jnss.me/application/o/your-app/.well-known/openid_configuration',
'client_kwargs': {
'scope': 'openid email profile groups'
}
}
# OAuth2 flow implementation
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register('authentik', **OAUTH2_CONFIG)
@app.route('/login')
def login():
redirect_uri = url_for('callback', _external=True)
return oauth.authentik.authorize_redirect(redirect_uri)
@app.route('/callback')
def callback():
token = oauth.authentik.authorize_access_token()
user_info = oauth.authentik.parse_id_token(token)
# Store user info in session
session['user'] = user_info
return redirect('/dashboard')
```
**Real-World Example**: See Example 1 below for Gitea OAuth/OIDC configuration.
---
### Pattern 2: Forward Authentication (Fallback Method)
**Use Case**: Existing HTTP services that don't support OAuth2/OIDC
**Benefits**:
- ✅ No application code changes required
- ✅ Consistent authentication across all services
- ✅ Service receives authenticated user information via headers
- ✅ Centralized session management
- ✅ Works with any HTTP application
**Limitations**:
- ⚠️ Non-standard authentication approach
- ⚠️ All requests must flow through authenticating proxy
- ⚠️ Limited logout functionality
- ⚠️ Backend must trust proxy-provided headers
**Implementation**: **Implementation**:
@@ -143,48 +215,9 @@ def dashboard():
return f"Welcome {user_name} ({username})" return f"Welcome {user_name} ({username})"
``` ```
### Pattern 2: OAuth2/OIDC Integration **Real-World Example**: See Example 3 below for static site protection with forward auth.
**Use Case**: Applications that can implement OAuth2 client functionality ---
**Benefits**:
- More control over authentication flow
- Better integration with application user models
- Support for API access tokens
- Offline access via refresh tokens
**Implementation**:
```python
# Example OAuth2 configuration
OAUTH2_CONFIG = {
'client_id': 'your-client-id',
'client_secret': 'your-client-secret',
'server_metadata_url': 'https://auth.jnss.me/application/o/your-app/.well-known/openid_configuration',
'client_kwargs': {
'scope': 'openid email profile groups'
}
}
# OAuth2 flow implementation
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register('authentik', **OAUTH2_CONFIG)
@app.route('/login')
def login():
redirect_uri = url_for('callback', _external=True)
return oauth.authentik.authorize_redirect(redirect_uri)
@app.route('/callback')
def callback():
token = oauth.authentik.authorize_access_token()
user_info = oauth.authentik.parse_id_token(token)
# Store user info in session
session['user'] = user_info
return redirect('/dashboard')
```
### Pattern 3: API-Only Authentication ### Pattern 3: API-Only Authentication
@@ -512,35 +545,69 @@ Remote-Session-ID: sess_abc123def456
## Integration Examples ## Integration Examples
### Example 1: Protecting Gitea with Groups ### Example 1: Gitea with OAuth2/OIDC Integration
**Objective**: Protect Git repository access with role-based permissions **Objective**: Git repository access with native OAuth2/OIDC authentication
**Why OAuth for Gitea**: Gitea has native OAuth2/OIDC support, providing better security and user experience than forward auth.
#### Step 1: Configure Authentik OAuth2 Provider
```yaml
# Authentik Provider Configuration
name: "Gitea OAuth Provider"
type: "OAuth2/OpenID Provider"
authorization_flow: "default-provider-authorization-implicit-consent"
client_type: "confidential"
client_id: "gitea"
redirect_uris: "https://git.jnss.me/user/oauth2/Authentik/callback"
signing_key: "authentik-default-key"
scopes: ["openid", "profile", "email", "groups"]
property_mappings:
- "authentik default OAuth Mapping: OpenID 'openid'"
- "authentik default OAuth Mapping: OpenID 'email'"
- "authentik default OAuth Mapping: OpenID 'profile'"
```
#### Step 2: Caddy Configuration
```caddyfile ```caddyfile
# Caddy configuration for Gitea # Caddy configuration for Gitea - No forward auth needed
git.jnss.me { git.jnss.me {
forward_auth https://auth.jnss.me {
uri /outpost.goauthentik.io/auth/caddy
copy_headers Remote-User Remote-Groups
}
reverse_proxy localhost:3000 reverse_proxy localhost:3000
} }
``` ```
**Gitea Configuration**: #### Step 3: Gitea Configuration
```ini
# app.ini - Gitea configuration
[auth]
REVERSE_PROXY_AUTHENTICATION = true
REVERSE_PROXY_AUTO_REGISTRATION = true
[auth.reverse_proxy] ```ini
USER_HEADER = Remote-User # app.ini - Gitea OAuth2 configuration
EMAIL_HEADER = Remote-Email [openid]
FULL_NAME_HEADER = Remote-Name ENABLE_OPENID_SIGNIN = false
ENABLE_OPENID_SIGNUP = false
[oauth2_client]
REGISTER_EMAIL_CONFIRM = false
OPENID_CONNECT_SCOPES = openid email profile groups
ENABLE_AUTO_REGISTRATION = true
USERNAME = preferred_username
EMAIL = email
ACCOUNT_LINKING = auto
``` ```
#### Step 4: Add OAuth Source in Gitea Web UI
1. Navigate to **Site Administration → Authentication Sources**
2. Click **Add Authentication Source**
3. **Authentication Type**: OAuth2
4. **Authentication Name**: Authentik
5. **OAuth2 Provider**: OpenID Connect
6. **Client ID**: `gitea` (from Authentik provider)
7. **Client Secret**: (from Authentik provider)
8. **OpenID Connect Auto Discovery URL**: `https://auth.jnss.me/application/o/gitea/.well-known/openid-configuration`
9. **Additional Scopes**: `profile email groups`
10. Enable: **Skip local 2FA**, **Automatically create users**
**Group Mapping**: **Group Mapping**:
```yaml ```yaml
# Authentik group configuration for Gitea # Authentik group configuration for Gitea
@@ -555,6 +622,8 @@ groups:
permissions: ["admin"] permissions: ["admin"]
``` ```
**Result**: Users see "Sign in with Authentik" button on Gitea login page with full OAuth flow.
### Example 2: API Service with Scoped Access ### Example 2: API Service with Scoped Access
**Objective**: REST API with OAuth2 authentication and scoped permissions **Objective**: REST API with OAuth2 authentication and scoped permissions

View File

@@ -9,7 +9,7 @@ This guide covers the complete deployment process for Authentik, a modern authen
- **Native PostgreSQL** - High-performance database with Unix socket IPC - **Native PostgreSQL** - High-performance database with Unix socket IPC
- **Native Valkey** - Redis-compatible cache with Unix socket IPC - **Native Valkey** - Redis-compatible cache with Unix socket IPC
- **Podman Containers** - System-level container orchestration via systemd/Quadlet - **Podman Containers** - System-level container orchestration via systemd/Quadlet
- **Caddy Reverse Proxy** - TLS termination and forward authentication - **Caddy Reverse Proxy** - TLS termination, OAuth2/OIDC provider, and forward authentication fallback
## Architecture Summary ## Architecture Summary
@@ -279,7 +279,9 @@ curl -s https://auth.jnss.me/api/v3/admin/version/
# Launch URL: Your application URL # Launch URL: Your application URL
``` ```
#### 3. Configure Forward Auth (for Caddy integration) #### 3. Configure Forward Auth (for services without OAuth support)
**Note**: Only use Forward Auth for services that don't support OAuth2/OIDC integration. For services like Nextcloud, Gitea, or Grafana, use OAuth2 providers (see step 1 above) instead.
```bash ```bash
# Navigate to Applications → Providers → Create # Navigate to Applications → Providers → Create
@@ -287,17 +289,72 @@ curl -s https://auth.jnss.me/api/v3/admin/version/
# Name: "Forward Auth Provider" # Name: "Forward Auth Provider"
# External Host: https://your-service.jnss.me # External Host: https://your-service.jnss.me
# Internal Host: http://localhost:8080 (your service backend) # Internal Host: http://localhost:8080 (your service backend)
# Use this only for: static sites, legacy apps, simple tools without OAuth support
``` ```
## Service Integration Examples ## Service Integration Examples
### Example 1: Protect Existing HTTP Service with Forward Auth ### Choosing the Right Integration Method
**Primary Method - OAuth2/OIDC** (Use when service supports it):
- ✅ **Nextcloud**: Native OIDC support
- ✅ **Gitea**: Native OAuth2 support
- ✅ **Grafana**: Native OAuth2 support
- ✅ **Custom applications**: Applications with OAuth2 client libraries
**Fallback Method - Forward Auth** (Use when service doesn't support OAuth):
- ⚠️ **Static sites**: No authentication capabilities
- ⚠️ **Legacy applications**: Cannot be modified
- ⚠️ **Simple tools**: whoami, monitoring dashboards without auth
---
### Example 1: OAuth2/OIDC Integration (Nextcloud)
**Recommended for services with native OAuth support**
For applications that can handle OAuth2/OIDC directly (like Nextcloud, Gitea):
```yaml
# Nextcloud OIDC configuration (config.php)
'oidc_login_provider_url' => 'https://auth.jnss.me/application/o/nextcloud/',
'oidc_login_client_id' => 'nextcloud-client-id',
'oidc_login_client_secret' => 'your_client_secret',
'oidc_login_auto_redirect' => true,
'oidc_login_end_session_redirect' => true,
'oidc_login_button_text' => 'Login with SSO',
'oidc_login_hide_password_form' => true,
'oidc_login_use_id_token' => true,
'oidc_login_attributes' => [
'id' => 'preferred_username',
'name' => 'name',
'mail' => 'email',
'groups' => 'groups',
],
'oidc_login_default_group' => 'users',
'oidc_login_scope' => 'openid profile email groups',
'oidc_login_tls_verify' => true,
```
**Caddy configuration** (no forward auth needed):
```caddyfile
# In /etc/caddy/sites-enabled/nextcloud.caddy
cloud.jnss.me {
reverse_proxy localhost:8080
}
```
---
### Example 2: Forward Auth for Services Without OAuth Support
**Use only when OAuth2/OIDC is not available**
Add to your service's Caddy configuration: Add to your service's Caddy configuration:
```caddyfile ```caddyfile
# In /etc/caddy/sites-enabled/myservice.caddy # In /etc/caddy/sites-enabled/whoami.caddy
myservice.jnss.me { whoami.jnss.me {
# Forward authentication to authentik # Forward authentication to authentik
forward_auth https://auth.jnss.me { forward_auth https://auth.jnss.me {
uri /outpost.goauthentik.io/auth/caddy uri /outpost.goauthentik.io/auth/caddy
@@ -309,146 +366,6 @@ myservice.jnss.me {
} }
``` ```
### Example 2: OAuth2 Integration for Custom Applications
For applications that can handle OAuth2 directly:
```yaml
# Application configuration
OAUTH2_PROVIDER_URL: "https://auth.jnss.me/application/o/authorize/"
OAUTH2_TOKEN_URL: "https://auth.jnss.me/application/o/token/"
OAUTH2_USER_INFO_URL: "https://auth.jnss.me/application/o/userinfo/"
OAUTH2_CLIENT_ID: "your_client_id"
OAUTH2_CLIENT_SECRET: "your_client_secret"
OAUTH2_REDIRECT_URI: "https://yourapp.jnss.me/oauth/callback"
```
## Troubleshooting Guide
### Common Issues and Solutions
#### Issue: Containers fail to start with socket permission errors
**Symptoms**:
```
Error: failed to connect to database: permission denied
```
**Solution**:
```bash
# Check authentik user group membership
ssh root@your-vps "groups authentik"
# Should show: authentik postgres-clients valkey-clients
# Verify container process groups
ssh root@your-vps "ps aux | grep authentik-server | head -1 | awk '{print \$2}' | xargs -I {} cat /proc/{}/status | grep Groups"
# Should show: Groups: 961 962 966 (valkey-clients postgres-clients authentik)
# Verify socket permissions
ssh root@your-vps "ls -la /var/run/postgresql/ /var/run/valkey/"
# Fix group membership if missing
ansible-playbook site.yml --tags authentik,user,setup --ask-vault-pass
```
#### Issue: HTTP binding errors (address already in use)
**Symptoms**:
```
Error: bind: address already in use (port 9000)
```
**Solution**:
```bash
# Check what's using port 9000
ssh root@your-vps "netstat -tulpn | grep 9000"
# Stop conflicting services
ssh root@your-vps "systemctl stop authentik-pod"
# Restart with correct configuration
ansible-playbook site.yml --tags authentik,containers --ask-vault-pass
```
#### Issue: Database connection failures
**Symptoms**:
```
FATAL: database "authentik" does not exist
```
**Solution**:
```bash
# Recreate database and user
ansible-playbook site.yml --tags authentik,database --ask-vault-pass
# Verify database creation
ssh root@your-vps "sudo -u postgres psql -h /var/run/postgresql -c '\l'"
```
#### Issue: Cache connection failures
**Symptoms**:
```
Error connecting to Redis: Connection refused
```
**Solution**:
```bash
# Check Valkey service status
ssh root@your-vps "systemctl status valkey"
# Test socket connectivity
ssh root@your-vps "redis-cli -s /var/run/valkey/valkey.sock ping"
# Redeploy cache configuration if needed
ansible-playbook site.yml --tags authentik,cache --ask-vault-pass
```
### Diagnostic Commands
#### Container Debugging
```bash
# Check container logs
ssh root@your-vps "podman logs authentik-server"
ssh root@your-vps "podman logs authentik-worker"
# Inspect container configuration
ssh root@your-vps "podman inspect authentik-server"
# Check container user/group mapping
ssh root@your-vps "podman exec authentik-server id"
# Expected: uid=966(authentik) gid=966(authentik) groups=966(authentik),961(valkey-clients),962(postgres-clients)
```
#### Service Status Verification
```bash
# Check all authentik systemd services
ssh root@your-vps "systemctl status authentik-pod authentik-server authentik-worker"
# View service dependencies
ssh root@your-vps "systemctl list-dependencies authentik-pod"
# Verify services are in system.slice
ssh root@your-vps "systemctl status authentik-server | grep CGroup"
# Expected: /system.slice/authentik-server.service
```
#### Network Connectivity Testing
```bash
# Test internal HTTP binding
ssh root@your-vps "curl -v http://127.0.0.1:9000/"
# Test Caddy reverse proxy
ssh root@your-vps "curl -v http://127.0.0.1:80/ -H 'Host: auth.jnss.me'"
# Test external HTTPS
curl -v https://auth.jnss.me/
```
### Log Analysis ### Log Analysis
#### Key Log Locations #### Key Log Locations
@@ -513,174 +430,3 @@ ssh root@your-vps "journalctl -u authentik-server --since today | grep '\"level\
ssh root@your-vps "journalctl -u authentik-server | grep '\"event\":\"database connection\"'" ssh root@your-vps "journalctl -u authentik-server | grep '\"event\":\"database connection\"'"
``` ```
## Performance Monitoring
### Resource Usage Monitoring
```bash
# Monitor container resource usage
ssh root@your-vps "podman stats"
# Monitor service memory usage
ssh root@your-vps "systemctl status authentik-server | grep Memory"
# Monitor database connections
ssh root@your-vps "sudo -u postgres psql -h /var/run/postgresql -c 'SELECT * FROM pg_stat_activity;'"
```
### Performance Optimization Tips
1. **Database Performance**:
- Monitor PostgreSQL slow query log
- Consider database connection pooling for high traffic
- Regular database maintenance (VACUUM, ANALYZE)
2. **Cache Performance**:
- Monitor Valkey memory usage and hit rate
- Adjust cache TTL settings based on usage patterns
- Consider cache warming for frequently accessed data
3. **Container Performance**:
- Monitor container memory limits and usage
- Optimize shared memory configuration if needed
- Review worker process configuration
## Maintenance Tasks
### Regular Maintenance
#### Update Authentik Version
```yaml
# Update version in defaults or inventory
authentik_version: "2025.12.1" # New version
# Deploy update
ansible-playbook site.yml --tags authentik,containers --ask-vault-pass
```
#### Backup Procedures
```bash
# Database backup
ssh root@your-vps "sudo -u postgres pg_dump -h /var/run/postgresql authentik > /backup/authentik-$(date +%Y%m%d).sql"
# Media files backup
ssh root@your-vps "tar -czf /backup/authentik-media-$(date +%Y%m%d).tar.gz -C /opt/authentik media"
# Configuration backup (run from ansible control machine)
ansible-vault view host_vars/arch-vps/vault.yml > backup/authentik-vault-$(date +%Y%m%d).yml
```
#### Health Monitoring
Set up regular health checks:
```bash
#!/bin/bash
# Health check script
HEALTH_URL="https://auth.jnss.me/if/health/live/"
if ! curl -f -s "$HEALTH_URL" > /dev/null; then
echo "Authentik health check failed"
# Add alerting logic
fi
```
### Security Maintenance
#### Certificate Monitoring
```bash
# Check certificate expiration
ssh root@your-vps "curl -vI https://auth.jnss.me/ 2>&1 | grep expire"
# Caddy handles renewal automatically, but monitor logs
ssh root@your-vps "journalctl -u caddy | grep -i cert"
```
#### Security Updates
```bash
# Update container images regularly
ansible-playbook site.yml --tags authentik,image-pull --ask-vault-pass
# Monitor for Authentik security advisories
# https://github.com/goauthentik/authentik/security/advisories
```
## Support and Resources
### Documentation References
- **Authentik Official Documentation**: https://docs.goauthentik.io/
- **rick-infra Architecture Decisions**: [docs/architecture-decisions.md](architecture-decisions.md)
- **Service Integration Guide**: [docs/service-integration-guide.md](service-integration-guide.md)
- **Security Model**: [docs/security-hardening.md](security-hardening.md)
### Community Resources
- **Authentik Community Forum**: https://community.goauthentik.io/
- **GitHub Issues**: https://github.com/goauthentik/authentik/issues
- **Discord Community**: https://discord.gg/jg33eMhnj6
### Emergency Procedures
#### Service Recovery
```bash
# Emergency service restart
ssh root@your-vps "systemctl restart authentik-pod"
# Fallback: Direct container management
ssh root@your-vps "podman pod restart authentik"
# Last resort: Full service rebuild
ansible-playbook site.yml --tags authentik --ask-vault-pass --limit arch-vps
```
#### Rollback Procedures
```bash
# Rollback to previous container version
authentik_version: "previous_working_version"
ansible-playbook site.yml --tags authentik,containers --ask-vault-pass
# Database rollback (if needed)
ssh root@your-vps "sudo -u postgres psql -h /var/run/postgresql authentik < /backup/authentik-backup.sql"
```
---
## Deployment Checklist
Use this checklist to ensure complete deployment:
### Pre-deployment
- [ ] Infrastructure services (PostgreSQL, Valkey, Caddy, Podman) running
- [ ] DNS records configured for auth.jnss.me
- [ ] Vault variables configured and encrypted
- [ ] Ansible connectivity verified
### Deployment
- [ ] Authentik role enabled in site.yml
- [ ] Deployment executed successfully
- [ ] Health checks passing
- [ ] Containers running and responsive
### Post-deployment
- [ ] Admin web interface accessible
- [ ] Initial admin login successful
- [ ] OAuth2 provider configured
- [ ] Test application integration
- [ ] Forward auth configuration tested
### Production Readiness
- [ ] Backup procedures implemented
- [ ] Monitoring and alerting configured
- [ ] Security review completed
- [ ] Documentation updated
- [ ] Team training completed
---
This comprehensive deployment guide provides everything needed to successfully deploy and maintain Authentik in the rick-infra environment, emphasizing the security and performance benefits of our native database + Unix socket architecture.

View File

@@ -43,21 +43,45 @@ The rick-infra deployment system provides:
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
## Infrastructure Overview
Rick-infra now manages **two separate environments**:
### Homelab (arch-vps)
Personal services and experimentation platform at **jnss.me**:
- PostgreSQL, Valkey, Podman infrastructure
- Caddy reverse proxy with auto-HTTPS
- Nextcloud (cloud.jnss.me)
- Authentik SSO (auth.jnss.me)
- Gitea (git.jnss.me)
### Production (mini-vps)
Client projects requiring high uptime:
- Caddy reverse proxy with auto-HTTPS
- Sigvild Gallery (sigvild.no, api.sigvild.no)
## Available Deployments ## Available Deployments
### 1. `site.yml` - Complete Infrastructure Stack ### 1. `site.yml` - Deploy All Environments
Deploys the full rick-infra stack with role dependencies automatically managed. Deploys both homelab and production infrastructure.
```bash ```bash
ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass ansible-playbook site.yml --ask-vault-pass
``` ```
**What it deploys:** ### 2. Environment-Specific Deployments
- **Security Foundation**: SSH hardening, firewall, fail2ban, system updates
- **Infrastructure Services**: PostgreSQL, Valkey, Podman container runtime ```bash
- **Reverse Proxy**: Caddy with automatic HTTPS and Cloudflare DNS integration # Deploy only homelab services
- **Authentication**: Authentik SSO server with forward auth integration ansible-playbook playbooks/homelab.yml --ask-vault-pass
- **Applications**: Gitea, Gallery, and other configured services
# Deploy only production services
ansible-playbook playbooks/production.yml --ask-vault-pass
# Or use site.yml with limits
ansible-playbook site.yml -l homelab --ask-vault-pass
ansible-playbook site.yml -l production --ask-vault-pass
```
### 2. Service-Specific Deployments ### 2. Service-Specific Deployments
Deploy individual components using tags: Deploy individual components using tags:
@@ -193,7 +217,21 @@ For complete authentik architecture details, see [Architecture Decisions](archit
## Configuration Management ## Configuration Management
### Host Variables ### Variable Organization
Rick-infra uses a hybrid approach for variable management:
**Group Variables** (`group_vars/`):
- `production/main.yml` - Production environment configuration
- `production/vault.yml` - Production secrets (encrypted)
**Host Variables** (`host_vars/`):
- `arch-vps/main.yml` - Homelab configuration
- `arch-vps/vault.yml` - Homelab secrets (encrypted)
**Note:** Due to variable loading issues in Ansible 2.20, playbooks manually load variables using `include_vars`. This ensures reliable variable resolution during execution.
### Example: Homelab Configuration
Core infrastructure settings in `host_vars/arch-vps/main.yml`: Core infrastructure settings in `host_vars/arch-vps/main.yml`:

View File

@@ -0,0 +1,414 @@
# Devigo Deployment Guide
**Status**: ✅ **DEPLOYED AND OPERATIONAL**
**Environment**: Production (mini-vps)
**Last Updated**: 2025-12-16
**Deployment Date**: 2025-12-15
## Overview
This guide documents the complete deployment of devigo.no (sales training website) from the legacy arch-vps server to the production mini-vps environment. The deployment uses:
- **Infrastructure Management**: Ansible (rick-infra repository)
- **Application Code**: Hugo static site (rustan repository)
- **CI/CD**: GitHub Actions
- **Container Runtime**: Podman with Quadlet/systemd integration
- **Web Server**: Caddy (reverse proxy with automatic HTTPS)
- **Content Management**: Decap CMS with GitHub OAuth
## Architecture
### Request Flow
```
Internet → Caddy (443) → nginx container (9080) → Static files
TLS termination
Security headers
Access logging
```
### Services
| Service | Domain | Container | Purpose |
|---------|--------|-----------|---------|
| devigo-site | devigo.no | ghcr.io/jnschaffer/rustan:prod | Main website |
| devigo-site | www.devigo.no | (redirects to apex) | WWW redirect |
| devigo-decap-oauth | decap.jnss.me | alukovenko/decapcms-oauth2 | OAuth proxy for CMS |
### OAuth Flow
```
1. User visits devigo.no/admin
2. Clicks "Login with GitHub"
3. Popup opens → decap.jnss.me/auth
4. Redirects to GitHub OAuth
5. GitHub redirects → decap.jnss.me/callback
6. Callback validates origin (TRUSTED_ORIGINS)
7. postMessage sends token to parent window
8. Popup closes, CMS authenticated ✅
```
### Deployment Flow
```
Developer → git push origin/prod
GitHub Actions
Build Hugo site → Docker image
Push to GHCR (ghcr.io/jnschaffer/rustan:prod)
SSH to mini-vps → podman pull (uses auth.json)
systemctl restart devigo-site
Podman Quadlet pulls latest image
Container starts with new code ✅
```
## Completed Deployment Steps ✅
### Infrastructure Configuration
- ✅ Created Ansible role `roles/devigo/` with Podman Quadlet support
- ✅ Updated `group_vars/production/main.yml` with devigo configuration
- ✅ Encrypted OAuth credentials in `group_vars/production/vault.yml`
- ✅ Updated `playbooks/production.yml` to include devigo role
- ✅ Deployed infrastructure to mini-vps
### Application Configuration
- ✅ Changed `rustan/hugo.toml` baseURL from `https://www.devigo.no/` to `https://devigo.no/`
- ✅ Updated `rustan/.github/workflows/deploy.yml` for Podman + systemctl deployment
- ✅ Fixed nginx.conf MIME type handling (location-specific YAML override)
- ✅ Implemented JavaScript-based Decap CMS initialization
### Authentication & Security
- ✅ Created GitHub Personal Access Token for GHCR
- ✅ Configured Podman authentication (`/etc/containers/auth.json`)
- ✅ Added `REGISTRY_AUTH_FILE` environment to Quadlet
- ✅ Fixed OAuth `TRUSTED_ORIGINS` environment variable (plural form)
- ✅ Generated SSH key for GitHub Actions deployment
- ✅ Updated GitHub repository secrets
### DNS & Networking
- ✅ Updated DNS records to point to mini-vps (72.62.91.251)
- ✅ Configured Caddy reverse proxy
- ✅ Obtained TLS certificates via Let's Encrypt
- ✅ Configured GitHub OAuth app callback URLs
### Testing & Verification
- ✅ Verified MIME types (HTML: text/html, CSS: text/css, YAML: text/yaml)
- ✅ Tested Decap CMS initialization
- ✅ Verified OAuth authentication flow
- ✅ Confirmed container image pulls work
- ✅ Tested automated GitHub Actions deployment
- ✅ Confirmed site accessible at devigo.no
## Lessons Learned
### Critical Fixes Applied
#### 1. TRUSTED_ORIGINS Variable Name (OAuth)
**Issue**: OAuth showing "Origin not trusted: https://devigo.no" error
**Root Cause**: The decapcms-oauth2 service source code expects:
- `TRUSTED_ORIGIN` (singular) for a single origin
- `TRUSTED_ORIGINS` (plural) for comma-separated multiple origins
We were using `TRUSTED_ORIGIN=https://devigo.no,https://www.devigo.no` which treated the entire string as ONE origin.
**Solution**: Changed to `TRUSTED_ORIGINS=https://devigo.no,https://www.devigo.no` (plural)
**Files Modified**:
- `roles/devigo/templates/devigo-decap-oauth.env.j2`
- `roles/devigo/defaults/main.yml`
#### 2. MIME Type Handling Strategy (nginx)
**Issue**: Adding `types { text/yaml yml yaml; }` in nginx.conf broke all MIME types:
- HTML → `application/octet-stream`
- CSS → `application/octet-stream`
- JS → `application/octet-stream`
**Root Cause**: Server-level `types {}` block **replaces** all MIME types, not extends them.
**Solution**: Use location-specific override instead:
```nginx
location ~ \.(yml|yaml)$ {
default_type text/yaml;
}
```
**Outcome**: Standard MIME types work, YAML gets special handling.
#### 3. Decap CMS Initialization Method
**Issue**: Decap CMS failing to load config with `<link href="config.yml" type="text/yaml">` approach
**Root Cause**: Even with JavaScript initialization, Decap CMS validates Content-Type header when fetching via link tag.
**Solution**: Switched to JavaScript manual initialization:
```javascript
window.CMS_MANUAL_INIT = true;
CMS.init({ config: { load_config_file: '/admin/config.yml' } });
```
**Benefits**:
- Eliminates YAML MIME type dependency for link element
- More portable across server configurations
- Matches working configuration from previous deployment
#### 4. Podman Authentication for systemd Services
**Issue**: `podman pull` worked manually but failed when systemd restarted service
**Root Cause**: Podman wasn't finding `/etc/containers/auth.json` when run by systemd
**Solution**: Added environment variable to Quadlet:
```ini
[Service]
Environment=REGISTRY_AUTH_FILE=/etc/containers/auth.json
```
**Outcome**: Automated deployments now pull private images successfully.
## Current Infrastructure State
### Services Status
| Service | Status | Container ID | Image Version |
|---------|--------|--------------|---------------|
| devigo-site | ✅ Running | a61788a8a673 | 2025-12-15T23:02:XX |
| devigo-decap-oauth | ✅ Running | 91fc8c4b21e3 | latest |
| caddy | ✅ Running | N/A | Native service |
### Verification Commands
```bash
# Check all services
ansible mini-vps -m shell -a "systemctl status devigo-site devigo-decap-oauth caddy --no-pager"
# View recent logs
ansible mini-vps -m shell -a "journalctl -u devigo-site -n 20 --no-pager"
# Check containers
ansible mini-vps -m shell -a "podman ps"
# Test endpoints
curl -I https://devigo.no
curl -I https://www.devigo.no # Should 301 redirect to apex
curl -I https://decap.jnss.me
# Verify MIME types
curl -I https://devigo.no/ | grep content-type # Should be text/html
curl -I https://devigo.no/admin/config.yml | grep content-type # Should be text/yaml
```
## Maintenance Guide
### Updating Content via Decap CMS
1. Visit https://devigo.no/admin
2. Click "Login with GitHub"
3. Edit content in the CMS interface
4. Changes automatically commit to GitHub
5. GitHub Actions builds and deploys new version
6. Site updates within 2-3 minutes
### Updating Application Code
```bash
# In rustan repository
cd ~/rustan
git checkout prod
# Make changes to code
git add .
git commit -m "Description of changes"
git push origin prod
# GitHub Actions automatically:
# 1. Builds Hugo site
# 2. Creates Docker image
# 3. Pushes to GHCR
# 4. SSHs to mini-vps
# 5. Pulls latest image
# 6. Restarts service
```
### Rotating GitHub PAT for GHCR
When the Personal Access Token expires:
```bash
# 1. Create new token at https://github.com/settings/tokens/new?scopes=read:packages
# 2. Update Ansible vault
cd ~/rick-infra
ansible-vault edit group_vars/production/vault.yml
# Update: vault_github_token: "ghp_NEW_TOKEN_HERE"
# 3. Deploy updated authentication
ansible-playbook playbooks/production.yml --tags podman
# 4. Verify
ansible mini-vps -m shell -a "podman login ghcr.io --get-login"
# Should show: jnschaffer
```
### Updating OAuth Trusted Origins
If you need to add/change trusted origins for OAuth:
```bash
# 1. Update defaults
cd ~/rick-infra
# Edit roles/devigo/defaults/main.yml
# Update: devigo_oauth_trusted_origins: "https://devigo.no,https://new-domain.com"
# 2. Deploy changes
ansible-playbook playbooks/production.yml --tags devigo
# 3. Restart OAuth service
ansible mini-vps -m shell -a "systemctl restart devigo-decap-oauth"
# 4. Test OAuth flow at https://devigo.no/admin
```
### Manual Container Updates
```bash
# Pull latest image manually
ansible mini-vps -m shell -a "podman pull ghcr.io/jnschaffer/rustan:prod"
# Restart service to use new image
ansible mini-vps -m shell -a "systemctl restart devigo-site"
# Check auto-update is working
ansible mini-vps -m shell -a "podman auto-update --dry-run"
```
## Troubleshooting
See the [Devigo Role README](../roles/devigo/README.md#troubleshooting) for comprehensive troubleshooting steps.
### Quick Reference
**OAuth not working?**
```bash
# Check TRUSTED_ORIGINS format
ansible mini-vps -m shell -a "cat /opt/devigo-oauth/decap-oauth.env | grep TRUSTED"
# Should be: TRUSTED_ORIGINS=https://devigo.no,https://www.devigo.no
```
**MIME types wrong?**
```bash
# Check config.yml MIME type
curl -I https://devigo.no/admin/config.yml | grep content-type
# Should be: content-type: text/yaml
# Check homepage MIME type
curl -I https://devigo.no/ | grep content-type
# Should be: content-type: text/html
```
**Can't pull images?**
```bash
# Test authentication
ansible mini-vps -m shell -a "podman login ghcr.io --get-login"
# Should show: jnschaffer (your GitHub username)
```
## Security Considerations
### Credentials Storage
All sensitive credentials are encrypted in Ansible Vault:
- GitHub OAuth Client ID & Secret
- GitHub Personal Access Token for GHCR
- Deployed files have restrictive permissions (0600 for secrets)
### Network Security
- TLS certificates automatically renewed via Caddy + Let's Encrypt
- All HTTP traffic redirected to HTTPS
- Security headers applied by Caddy:
- X-Frame-Options: SAMEORIGIN
- X-Content-Type-Options: nosniff
- X-XSS-Protection: 1; mode=block
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: geolocation=(), microphone=(), camera=()
### Container Security
- Containers run as non-root where possible
- `NoNewPrivileges=true` in Quadlet files
- Minimal attack surface (nginx:alpine base image)
- Regular updates via GitHub Actions rebuild
## Related Documentation
- [Devigo Role README](../roles/devigo/README.md) - Role configuration and usage
- [Podman Role](../roles/podman/README.md) - Container runtime setup
- [Caddy Role](../roles/caddy/README.md) - Reverse proxy configuration
- [Service Integration Guide](./service-integration-guide.md) - How services work together
## Migration Notes
### Differences from arch-vps Deployment
| Aspect | arch-vps (old) | mini-vps (new) |
|--------|----------------|----------------|
| Container | Docker + docker-compose | Podman + Quadlet |
| Service Mgmt | docker-compose | systemd |
| Deployment | Manual / docker-compose pull | GitHub Actions + systemctl |
| OAuth | Same service | Same, but TRUSTED_ORIGINS fixed |
| CMS Init | Link tag (fragile) | JavaScript (reliable) |
| MIME Types | Not documented | Location-specific override |
### Why We Migrated
1. **Better systemd integration** - Quadlet generates native systemd units
2. **Ansible consistency** - Declarative infrastructure management
3. **Improved security** - Podman rootless capable, no daemon
4. **Auto-updates** - Podman auto-update support
5. **Better logging** - Native journald integration
## Success Metrics
- ✅ Site accessible at https://devigo.no
- ✅ WWW redirect working (www.devigo.no → devigo.no)
- ✅ TLS certificates valid and auto-renewing
- ✅ OAuth authentication functional
- ✅ Decap CMS loads and allows content editing
- ✅ GitHub Actions automated deployment working
- ✅ All MIME types correct (HTML, CSS, JS, YAML)
- ✅ Zero downtime during migration
- ✅ All content preserved from previous deployment
## Future Improvements
### Potential Enhancements
1. **Monitoring**: Add Prometheus metrics collection
2. **Backups**: Automated backup of Decap CMS content (GitHub is primary backup)
3. **Staging Environment**: Deploy to separate staging domain for testing
4. **CDN**: Consider Cloudflare CDN for improved global performance
5. **Image Optimization**: Automated image optimization in build pipeline
6. **A/B Testing**: Framework for testing landing page variations
### Technical Debt
None identified - deployment is clean and well-documented.
## Contact & Support
For issues or questions:
1. Check this guide's troubleshooting section
2. Review role README files
3. Check systemd/container logs
4. Consult Ansible playbook output
---
**Deployment Status**: ✅ **COMPLETE AND OPERATIONAL**
**Next Review**: When rotating GitHub PAT (1 year from creation)

View File

@@ -0,0 +1,541 @@
# Gitea Deployment and Testing Guide
Comprehensive guide for deploying and testing Gitea Git service with SSH access on rick-infra.
## Deployment
### 1. Prerequisites
Ensure you have the required vault variables set in your host_vars:
```yaml
# host_vars/arch-vps/vault.yml (encrypted)
vault_gitea_db_password: "your_secure_password_here"
```
### 2. Deploy Gitea Role
Run the rick-infra playbook with Gitea role:
```bash
# Deploy Gitea to arch-vps
ansible-playbook -i inventory/hosts.yml rick-infra.yml --limit arch-vps
# Or deploy only Gitea role
ansible-playbook -i inventory/hosts.yml rick-infra.yml --tags gitea --limit arch-vps
```
### 3. Verify Deployment
Check that all services are running:
```bash
# SSH into the server
ssh root@arch-vps
# Check Gitea service status
systemctl status gitea
# Check if Gitea is listening on HTTP port
ss -tlnp | grep 3000
# Check if Gitea SSH is listening
ss -tlnp | grep 2222
# Verify firewall rules
nft list ruleset | grep 2222
# Check fail2ban status
fail2ban-client status gitea-ssh
```
Expected output:
- Gitea service: `active (running)`
- HTTP port 3000: listening on `127.0.0.1:3000`
- SSH port 2222: listening on `0.0.0.0:2222`
- nftables: Rule allowing `tcp dport 2222`
- fail2ban: `gitea-ssh` jail active
## Testing Guide
### Test 1: Web Interface Access
**Purpose**: Verify HTTPS access through Caddy reverse proxy
```bash
# From your local machine
curl -I https://git.jnss.me
```
**Expected result**:
- HTTP/2 200 OK
- Redirects to login page
- Valid TLS certificate
**Action**: Open browser to `https://git.jnss.me` and verify web interface loads
---
### Test 2: Firewall Port Verification
**Purpose**: Confirm port 2222 is accessible from external networks
```bash
# From your local machine (not from the server)
nc -zv git.jnss.me 2222
```
**Expected result**:
```
Connection to git.jnss.me 2222 port [tcp/*] succeeded!
```
**If this fails**: The firewall rule is not active or nftables service is not running.
**Troubleshooting**:
```bash
# On the server
ssh root@arch-vps
# Check if nftables service is running
systemctl status nftables
# List all firewall rules
nft list ruleset
# Verify Gitea rule file exists
cat /etc/nftables.d/gitea.nft
# Manually reload nftables
systemctl reload nftables
```
---
### Test 3: SSH Connection Test
**Purpose**: Verify Gitea SSH server accepts connections
```bash
# From your local machine
ssh -T -p 2222 git@git.jnss.me
```
**Expected result** (before adding SSH key):
```
Hi there, You've successfully authenticated, but Gitea does not provide shell access.
If this is unexpected, please log in with password and setup Gitea under another user.
```
**OR** (if authentication fails):
```
git@git.jnss.me: Permission denied (publickey).
```
This is normal - it means Gitea SSH server is responding, you just need to add your SSH key.
**If connection times out**: Port 2222 is blocked or Gitea SSH is not running.
---
### Test 4: SSH Key Setup and Authentication
**Purpose**: Add SSH key to Gitea and test authentication
**Step 4.1**: Create Gitea admin account
1. Visit `https://git.jnss.me`
2. Click "Register" (if registration is enabled) or use initial admin setup
3. Create your user account
**Step 4.2**: Generate SSH key (if needed)
```bash
# On your local machine
ssh-keygen -t ed25519 -C "your_email@example.com"
# View your public key
cat ~/.ssh/id_ed25519.pub
```
**Step 4.3**: Add SSH key to Gitea
1. Log into Gitea web interface
2. Click your profile → Settings
3. Click "SSH / GPG Keys" tab
4. Click "Add Key"
5. Paste your public key (`id_ed25519.pub` contents)
6. Give it a name and click "Add Key"
**Step 4.4**: Test SSH authentication
```bash
# From your local machine
ssh -T -p 2222 git@git.jnss.me
```
**Expected result**:
```
Hi there, <your_username>! You've successfully authenticated with the key named <key_name>
```
---
### Test 5: Repository Operations
**Purpose**: Test actual Git operations over SSH
**Step 5.1**: Create a test repository in Gitea web interface
1. Click "+" → "New Repository"
2. Name: `test-repo`
3. Click "Create Repository"
**Step 5.2**: Clone the repository
```bash
# From your local machine
git clone ssh://git@git.jnss.me:2222/your_username/test-repo.git
cd test-repo
```
**Expected result**: Repository clones successfully
**Step 5.3**: Make a commit and push
```bash
# Create a test file
echo "# Test Repository" > README.md
# Commit and push
git add README.md
git commit -m "Initial commit"
git push origin main
```
**Expected result**:
```
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 234 bytes | 234.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To ssh://git.jnss.me:2222/your_username/test-repo.git
* [new branch] main -> main
```
**Step 5.4**: Verify in web interface
1. Refresh Gitea web UI
2. Navigate to `test-repo`
3. Verify `README.md` appears
---
### Test 6: fail2ban Protection
**Purpose**: Verify SSH brute force protection is active
**Step 6.1**: Check fail2ban status
```bash
# On the server
ssh root@arch-vps
# Check gitea-ssh jail
fail2ban-client status gitea-ssh
```
**Expected result**:
```
Status for the jail: gitea-ssh
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- File list: /var/lib/gitea/log/gitea.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
```
**Step 6.2**: Simulate failed authentication (optional)
```bash
# From your local machine, try connecting with wrong key multiple times
ssh -T -p 2222 -i /path/to/wrong/key git@git.jnss.me
# Repeat this 5+ times quickly
```
**Step 6.3**: Check if IP was banned
```bash
# On the server
fail2ban-client status gitea-ssh
```
**Expected result**: Your IP should appear in "Currently banned" list after 5 failed attempts.
**Step 6.4**: Unban yourself (if needed)
```bash
# On the server
fail2ban-client set gitea-ssh unbanip YOUR_IP_ADDRESS
```
---
### Test 7: Firewall Rule Persistence
**Purpose**: Verify firewall rules survive reboot
**Step 7.1**: Check current rules
```bash
# On the server
ssh root@arch-vps
nft list ruleset | grep 2222
```
**Step 7.2**: Reboot server
```bash
# On the server
reboot
```
**Step 7.3**: After reboot, verify rules are still active
```bash
# Wait for server to come back up, then SSH in
ssh root@arch-vps
# Check nftables rules again
nft list ruleset | grep 2222
# Verify Gitea SSH still accessible from outside
exit
# From local machine
ssh -T -p 2222 git@git.jnss.me
```
**Expected result**: Port 2222 rule persists after reboot, SSH access still works.
---
## Troubleshooting
### Issue: Connection timeout on port 2222
**Symptoms**: `ssh: connect to host git.jnss.me port 2222: Connection timed out`
**Diagnosis**:
```bash
# On server
systemctl status gitea # Check if Gitea is running
ss -tlnp | grep 2222 # Check if SSH is listening
nft list ruleset | grep 2222 # Check firewall rule
systemctl status nftables # Check firewall service
```
**Solutions**:
1. **Gitea not running**: `systemctl start gitea`
2. **Firewall rule missing**: Re-run Ansible playbook with Gitea role
3. **nftables not running**: `systemctl start nftables`
4. **Rule file missing**: Check `/etc/nftables.d/gitea.nft` exists
---
### Issue: Permission denied (publickey)
**Symptoms**: SSH connection succeeds but authentication fails
**Diagnosis**:
```bash
# Verbose SSH connection
ssh -vvv -T -p 2222 git@git.jnss.me
```
**Solutions**:
1. **SSH key not added to Gitea**: Add your public key in Gitea web UI
2. **Wrong SSH key used**: Specify correct key: `ssh -i ~/.ssh/id_ed25519 -T -p 2222 git@git.jnss.me`
3. **Key permissions wrong**: `chmod 600 ~/.ssh/id_ed25519`
---
### Issue: fail2ban not protecting Gitea
**Symptoms**: `fail2ban-client status gitea-ssh` shows jail doesn't exist
**Diagnosis**:
```bash
# Check if filter exists
ls -la /etc/fail2ban/filter.d/gitea-ssh.conf
# Check if jail is configured
grep -A 10 "gitea-ssh" /etc/fail2ban/jail.local
# Check fail2ban logs
journalctl -u fail2ban | grep gitea
```
**Solutions**:
1. **Jail not configured**: Re-run Ansible playbook with Gitea role
2. **fail2ban not running**: `systemctl start fail2ban`
3. **Log file not found**: Check Gitea is logging to `/var/lib/gitea/log/gitea.log`
---
### Issue: Git clone works but push fails
**Symptoms**: Can clone but `git push` gives permission error
**Diagnosis**:
- Check repository permissions in Gitea web UI
- Verify you have write access to the repository
**Solutions**:
1. **Not repository owner**: Ask owner to give you write access
2. **Repository is archived**: Unarchive in settings
3. **Branch protected**: Check branch protection rules
---
## Verification Checklist
Use this checklist to verify your Gitea deployment:
- [ ] Gitea web interface accessible at `https://git.jnss.me`
- [ ] Port 2222 accessible from external network (`nc -zv git.jnss.me 2222`)
- [ ] SSH connection succeeds (`ssh -T -p 2222 git@git.jnss.me`)
- [ ] SSH key added to Gitea account
- [ ] SSH authentication works (shows username in response)
- [ ] Can clone repository via SSH
- [ ] Can push commits to repository
- [ ] nftables rule for port 2222 exists and is active
- [ ] fail2ban jail `gitea-ssh` is running
- [ ] Gitea service auto-starts on boot
- [ ] nftables rules persist after reboot
---
## Post-Deployment Configuration
### Disable Public Registration (Recommended)
If you don't want anyone to create accounts:
1. Edit `host_vars/arch-vps/main.yml` or `group_vars/production/main.yml`
2. Add:
```yaml
gitea_disable_registration: true
```
3. Re-run playbook:
```bash
ansible-playbook -i inventory/hosts.yml rick-infra.yml --tags gitea --limit arch-vps
```
### Configure Email (Optional)
For password resets and notifications, configure SMTP:
1. Edit Gitea configuration directly:
```bash
ssh root@arch-vps
nano /etc/gitea/app.ini
```
2. Add mailer section:
```ini
[mailer]
ENABLED = true
FROM = gitea@jnss.me
PROTOCOL = smtps
SMTP_ADDR = smtp.example.com
SMTP_PORT = 465
USER = gitea@jnss.me
PASSWD = your_smtp_password
```
3. Restart Gitea:
```bash
systemctl restart gitea
```
### Enable Actions/CI (Optional)
Gitea Actions provides GitHub Actions-compatible CI/CD:
1. Edit `roles/gitea/templates/app.ini.j2`
2. Add Actions section
3. Re-run playbook
---
## nftables Architecture
### Modular Firewall Design
Rick-infra uses a modular nftables architecture that allows services to self-manage their firewall rules:
**Load Order:**
1. **Base rules** (`/etc/nftables.conf`) - Infrastructure essentials (SSH, HTTP, HTTPS, ICMP)
2. **Service rules** (`/etc/nftables.d/[0-8]*.nft`) - Service-specific ports (e.g., `50-gitea.nft`)
3. **Drop rule** (`/etc/nftables.d/99-drop.nft`) - Final catch-all drop
**Key Files:**
- `/etc/nftables.conf` - Base infrastructure firewall rules
- `/etc/nftables-load.conf` - Loader script that orchestrates rule loading
- `/etc/nftables.d/50-gitea.nft` - Gitea SSH port (2222) rule
- `/etc/nftables.d/99-drop.nft` - Final drop rule (loaded last)
**How It Works:**
```
┌─────────────────────────────────────────────────┐
│ /etc/nftables-load.conf │
│ │
│ 1. include "/etc/nftables.conf" │
│ └─> Allow: SSH(22), HTTP(80), HTTPS(443) │
│ │
│ 2. include "/etc/nftables.d/[0-8]*.nft" │
│ └─> 50-gitea.nft: Allow SSH(2222) │
│ │
│ 3. include "/etc/nftables.d/99-drop.nft" │
│ └─> Drop all other traffic │
└─────────────────────────────────────────────────┘
```
This ensures service rules are evaluated **before** the drop rule, allowing each service role to be self-contained.
## Security Best Practices
1. **Use strong database password**: Ensure `vault_gitea_db_password` is strong
2. **Enable 2FA**: Enable two-factor authentication in Gitea settings
3. **Monitor fail2ban**: Regularly check banned IPs: `fail2ban-client status gitea-ssh`
4. **Keep updated**: Run security playbook regularly for system updates
5. **Review SSH keys**: Periodically audit SSH keys in Gitea user accounts
6. **Backup repositories**: Regular backups of `/var/lib/gitea/repositories`
7. **Monitor logs**: Check Gitea logs for suspicious activity: `journalctl -u gitea`
---
## Quick Reference Commands
```bash
# Service management
systemctl status gitea
systemctl restart gitea
journalctl -u gitea -f
# Firewall
nft list ruleset | grep 2222
systemctl restart nftables
cat /etc/nftables.d/50-gitea.nft
# fail2ban
fail2ban-client status gitea-ssh
fail2ban-client get gitea-ssh banned
fail2ban-client set gitea-ssh unbanip IP_ADDRESS
# Network
ss -tlnp | grep 2222
nc -zv git.jnss.me 2222
# SSH testing
ssh -T -p 2222 git@git.jnss.me
ssh -vvv -T -p 2222 git@git.jnss.me # Verbose mode
# Git operations
git clone ssh://git@git.jnss.me:2222/user/repo.git
git remote add origin ssh://git@git.jnss.me:2222/user/repo.git
```
---
**Rick-Infra Gitea Deployment Guide**
Self-contained Git service with automatic firewall and security management.

View File

@@ -0,0 +1,211 @@
# Gitea Email Configuration Troubleshooting
## Summary
Attempted to configure Gitea email functionality using Titan Email (Hostinger) SMTP service. Email sending is currently **non-functional** due to SMTP authentication rejection by Titan Email servers.
## Configuration Details
### Email Provider
- **Provider:** Titan Email (by Hostinger)
- **Account:** hello@jnss.me
- **SMTP Server:** smtp.titan.email
- **Ports Tested:** 587 (STARTTLS), 465 (SSL/TLS)
### Gitea Configuration
```ini
[mailer]
ENABLED = true
PROTOCOL = smtp+starttls
SMTP_ADDR = smtp.titan.email
SMTP_PORT = 587
FROM = hello@jnss.me
USER = hello@jnss.me
PASSWD = <vault_gitea_smtp_password>
SUBJECT_PREFIX = [Gitea]
SEND_AS_PLAIN_TEXT = false
SMTP_AUTH = PLAIN
```
## Issue Description
Gitea fails to send emails with the following error:
```
Failed to send emails: failed to authenticate SMTP: 535 5.7.8 Error: authentication failed
```
## Troubleshooting Performed
### 1. Credential Verification
-**Webmail access:** Successfully logged into https://mail.titan.email/ with credentials
-**Send/Receive:** Can send and receive emails through webmail interface
-**Password confirmed:** Tested multiple times, credentials are correct
### 2. SMTP Connectivity Tests
-**Port 587 (STARTTLS):** Connection successful, TLS upgrade successful
-**Port 465 (SSL/TLS):** Connection successful with implicit TLS
-**DNS Resolution:** smtp.titan.email resolves correctly to multiple IPs
### 3. Authentication Method Testing
**Manual SMTP tests from VPS (69.62.119.31):**
```python
# Test Results:
AUTH PLAIN: 535 5.7.8 Error: authentication failed
AUTH LOGIN: 535 5.7.8 Error: authentication failed
```
**Both authentication methods rejected by server despite correct credentials.**
### 4. Configuration Iterations Tested
#### Iteration 1: Port 465 with smtps
```ini
PROTOCOL = smtps
SMTP_PORT = 465
```
**Result:** Authentication failed (535)
#### Iteration 2: Port 587 with smtp+starttls
```ini
PROTOCOL = smtp+starttls
SMTP_PORT = 587
```
**Result:** Authentication failed (535)
#### Iteration 3: Explicit AUTH PLAIN
```ini
PROTOCOL = smtp+starttls
SMTP_PORT = 587
SMTP_AUTH = PLAIN
```
**Result:** Authentication failed (535)
#### Iteration 4: Removed conflicting TLS settings
Removed:
- `ENABLE_TLS = true` (conflicted with PROTOCOL)
- `SKIP_VERIFY = false` (deprecated)
**Result:** Authentication still failed (535)
### 5. Debug Output Analysis
SMTP conversation debug output revealed:
```
send: 'AUTH PLAIN AGhlbGxvQGpuc3MubWUASGVsbG8xMjMh\r\n'
reply: b'535 5.7.8 Error: authentication failed: \r\n'
send: 'AUTH LOGIN aGVsbG8Aam5zcy5tZQ==\r\n'
reply: b'334 UGFzc3dvcmQ6\r\n'
send: 'SGVsbG8xMjMh\r\n'
reply: b'535 5.7.8 Error: authentication failed: UGFzc3dvcmQ6\r\n'
```
**Analysis:** Server accepts both AUTH PLAIN and AUTH LOGIN in EHLO response but rejects actual authentication attempts for both methods.
## Root Cause Analysis
### What Works
- ✅ SMTP server connectivity (both ports)
- ✅ TLS/STARTTLS negotiation
- ✅ Webmail authentication with same credentials
- ✅ Email sending through webmail
### What Doesn't Work
- ❌ SMTP AUTH PLAIN from VPS
- ❌ SMTP AUTH LOGIN from VPS
- ❌ Both fail with identical error: 535 5.7.8
### Conclusion
**The issue is NOT a Gitea configuration problem.** The SMTP server is actively rejecting authentication attempts despite:
- Correct credentials (verified in webmail)
- Proper TLS establishment
- Correct authentication protocol usage
## Possible Causes
1. **SMTP Access Disabled:** Titan Email may require SMTP/IMAP access to be explicitly enabled in Hostinger control panel or Titan settings
2. **IP-Based Restrictions:** VPS IP (69.62.119.31) may be blocked or require whitelisting
3. **Account Verification Required:** Account may need additional verification for SMTP access
4. **Service-Level Restriction:** Titan Email plan may not include SMTP access for external applications
5. **Missing Activation:** SMTP feature may require separate activation from webmail access
## Attempted Solutions
### Configuration Changes
- [x] Tested both port 587 (STARTTLS) and 465 (SSL/TLS)
- [x] Tried AUTH PLAIN and AUTH LOGIN methods
- [x] Removed conflicting TLS settings (ENABLE_TLS, SKIP_VERIFY)
- [x] Updated password in vault and redeployed
- [x] Verified minimal clean configuration
### External Tests
- [ ] Test SMTP from different IP (local machine vs VPS)
- [ ] Check Hostinger control panel for SMTP toggle
- [ ] Contact Hostinger/Titan support
- [ ] Verify account has SMTP privileges
## Recommendations
### Immediate Next Steps
1. **Check Hostinger Control Panel:**
- Log into hpanel.hostinger.com
- Navigate to Emails → hello@jnss.me
- Look for SMTP/IMAP access toggle or settings
2. **Test from Different IP:**
- Test SMTP authentication from local machine
- If successful: IP blocking issue (request VPS IP whitelist)
- If failed: Account-level restriction
3. **Contact Support:**
- Provide error: "535 5.7.8 authentication failed"
- Request SMTP access verification for hello@jnss.me
- Ask if SMTP requires separate activation
### Alternative Email Solutions
If Titan Email SMTP cannot be resolved:
1. **Use Different Email Provider:**
- Gmail (with App Passwords)
- SendGrid (free tier: 100 emails/day)
- Mailgun (free tier: 5,000 emails/month)
- AWS SES (free tier: 62,000 emails/month)
2. **Use Local Mail Server:**
- Install Postfix on VPS
- Configure as relay
- More complex but full control
3. **Disable Email Features:**
- Set `ENABLED = false` in [mailer]
- OAuth account linking won't work
- Password reset requires admin intervention
- No email notifications
## Current Status
**Email functionality: DISABLED**
Configuration is correct but non-functional due to SMTP authentication rejection by Titan Email servers.
## Files Modified
- `roles/gitea/defaults/main.yml` - Email configuration variables
- `roles/gitea/templates/app.ini.j2` - Mailer section configuration
- `host_vars/arch-vps/vault.yml` - SMTP password
## References
- Gitea Mailer Documentation: https://docs.gitea.com/administration/config-cheat-sheet#mailer-mailer
- SMTP Error Codes: https://www.greenend.org.uk/rjk/tech/smtpreplies.html
- Titan Email Settings: https://support.hostinger.com/en/collections/3363865-titan-email
---
**Date:** 2025-12-19
**Investigated by:** OpenCode AI Assistant
**Status:** Unresolved - Awaiting Titan Email SMTP access verification

View File

@@ -0,0 +1,245 @@
# Gitea SSH Migration Guide
Guide for migrating between Gitea SSH modes and updating Git remote URLs.
## SSH Modes Overview
### Passthrough Mode (Default)
- **Port**: 22 (standard SSH)
- **URL Format**: `git@git.jnss.me:user/repo.git`
- **Security**: System fail2ban protects all SSH traffic
- **Recommended**: ✅ For production use
### Dedicated Mode (Fallback)
- **Port**: 2222 (Gitea SSH server)
- **URL Format**: `ssh://git@git.jnss.me:2222/user/repo.git`
- **Security**: Separate fail2ban jail for port 2222
- **Use Case**: Debugging or when passthrough has issues
---
## Migration: Dedicated → Passthrough (Default)
When you deploy the new code, Gitea will automatically switch to passthrough mode.
### What Happens Automatically
1. ✅ Gitea's SSH server stops listening on port 2222
2. ✅ Port 2222 firewall rule removed
3. ✅ System SSH configured for Git passthrough
4. ✅ AuthorizedKeysCommand script deployed
5. ✅ fail2ban switches to system `sshd` jail
### What You Need to Do
**Update your Git remote URLs** in each repository:
```bash
# Check current remote URL
git remote -v
# Update to new format (no port number)
git remote set-url origin git@git.jnss.me:username/repo.git
# Verify new URL
git remote -v
# Test connection
git fetch
```
### Bulk Update Script
If you have many repositories, use this script:
```bash
#!/bin/bash
# migrate-git-urls.sh - Update all Git remotes from dedicated to passthrough
# Find all git repositories in current directory and subdirectories
find . -type d -name '.git' | while read gitdir; do
repo=$(dirname "$gitdir")
echo "Processing: $repo"
cd "$repo"
# Get current origin URL
current_url=$(git remote get-url origin 2>/dev/null)
# Check if it's the old format (with :2222)
if [[ $current_url == *":2222/"* ]]; then
# Convert to new format
new_url=$(echo "$current_url" | sed 's|ssh://git@git.jnss.me:2222/|git@git.jnss.me:|')
echo " Old: $current_url"
echo " New: $new_url"
git remote set-url origin "$new_url"
echo " ✅ Updated"
else
echo " Already using correct format or not Gitea"
fi
cd - > /dev/null
echo ""
done
echo "Migration complete!"
```
**Usage:**
```bash
chmod +x migrate-git-urls.sh
./migrate-git-urls.sh
```
---
## Migration: Passthrough → Dedicated
If you need to switch back to dedicated mode:
### 1. Update Configuration
Edit `host_vars/arch-vps/main.yml`:
```yaml
gitea_ssh_mode: "dedicated"
```
### 2. Deploy
```bash
ansible-playbook -i inventory/hosts.yml rick-infra.yml --limit arch-vps
```
### 3. Update Git Remotes
```bash
# Update to dedicated format (with :2222 port)
git remote set-url origin ssh://git@git.jnss.me:2222/username/repo.git
# Test connection
ssh -T -p 2222 git@git.jnss.me
git fetch
```
---
## URL Format Reference
### Passthrough Mode (Port 22)
```bash
# Clone
git clone git@git.jnss.me:username/repo.git
# Add remote
git remote add origin git@git.jnss.me:username/repo.git
# SSH test
ssh -T git@git.jnss.me
```
### Dedicated Mode (Port 2222)
```bash
# Clone
git clone ssh://git@git.jnss.me:2222/username/repo.git
# Add remote
git remote add origin ssh://git@git.jnss.me:2222/username/repo.git
# SSH test
ssh -T -p 2222 git@git.jnss.me
```
---
## Troubleshooting
### After Migration, Git Operations Fail
**Symptom**: `git push` fails with "Permission denied" or "Connection refused"
**Solution**:
1. Check your remote URL format:
```bash
git remote -v
```
2. Update if needed:
```bash
# For passthrough (no port)
git remote set-url origin git@git.jnss.me:username/repo.git
# For dedicated (with port)
git remote set-url origin ssh://git@git.jnss.me:2222/username/repo.git
```
3. Test SSH connection:
```bash
# Passthrough
ssh -T git@git.jnss.me
# Dedicated
ssh -T -p 2222 git@git.jnss.me
```
### SSH Key Not Recognized After Migration
**Symptom**: "Permission denied (publickey)"
**Cause**: SSH keys are stored in Gitea's database, not affected by mode change.
**Solution**:
1. Verify your SSH key is in Gitea:
- Log into Gitea web interface
- Go to Settings → SSH/GPG Keys
- Check your key is listed
2. Test key locally:
```bash
ssh-add -l # List loaded keys
```
3. Try with explicit key:
```bash
ssh -T -i ~/.ssh/id_ed25519 git@git.jnss.me
```
### Port 2222 Still Open After Switching to Passthrough
**Symptom**: `nc -zv git.jnss.me 2222` succeeds
**Cause**: Gitea service may still be running on port 2222
**Solution**:
```bash
# On the server
systemctl restart gitea
ss -tlnp | grep 2222 # Should show nothing
```
---
## Verification Checklist
After migration, verify:
- [ ] SSH connection works: `ssh -T git@git.jnss.me` (passthrough) or `ssh -T -p 2222 git@git.jnss.me` (dedicated)
- [ ] Can clone repository with new URL format
- [ ] Can push commits to repository
- [ ] fail2ban is active: `fail2ban-client status sshd` (passthrough) or `fail2ban-client status gitea-ssh` (dedicated)
- [ ] Firewall configured correctly: `nft list ruleset | grep 2222` (should show nothing in passthrough)
---
## Notes
- **Both modes are fully supported** - choose what works best for your setup
- **No data loss** - repositories, users, and SSH keys are unaffected by mode changes
- **Gradual migration** - you can update remote URLs at your own pace (old URLs may still work for a short time)
- **Team coordination** - if you're in a team, coordinate the migration so everyone updates their URLs
---
**Rick-Infra Gitea SSH Migration Guide**
Switch between passthrough and dedicated SSH modes safely.

326
docs/jnss-web-deployment.md Normal file
View File

@@ -0,0 +1,326 @@
# jnss-web Static Site Deployment Guide
## Overview
This document describes the deployment process for jnss-web, a SvelteKit-based static website serving as the primary entrypoint for jnss.me.
## Architecture
- **Technology**: SvelteKit v2 with static adapter
- **Build Tool**: Bun
- **Deployment**: Ansible playbook with git-based artifact deployment
- **Web Server**: Caddy (direct file serving)
- **Repository**: git.jnss.me/joakim/jnss-web
- **Branch Strategy**:
- `main` - Source code
- `deploy` - Build artifacts only
## Server Architecture
```
/opt/jnss-web-repo/ # Git clone of deploy branch
/var/www/jnss-web/ # Synced build artifacts (served by Caddy)
/etc/caddy/sites-enabled/
└── jnss-web.caddy # Site config with www redirect
/var/log/caddy/jnss-web.log # Access logs
```
## Development Workflow
### 1. Local Development (main branch)
```bash
cd ~/dev/jnss-web
git checkout main
# Make changes to source code
# ...
# Test locally
bun run dev
# Build to verify
bun run build
```
### 2. Prepare Deploy Branch
```bash
# Build production version
bun run build
# Switch to deploy branch
git checkout deploy
# Merge changes from main
git merge main --no-commit
# Rebuild to ensure fresh build
bun run build
# Add only build artifacts
git add build/
# Commit with descriptive message
git commit -m "Deploy: Add new feature X"
# Push to Gitea
git push origin deploy
```
### 3. Deploy to Server
```bash
cd ~/rick-infra
# Run deployment playbook
ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml
# Watch logs (optional)
ssh root@arch-vps 'tail -f /var/log/caddy/jnss-web.log'
```
## Playbook Details
### Variables
Located in `playbooks/deploy-jnss-web.yml`:
- `jnss_web_repo_owner`: Your Gitea username (default: "joakim")
- `jnss_web_branch`: Branch to deploy (default: "deploy")
- `jnss_web_domain`: Primary domain (default: "jnss.me")
### Tags
- `jnss-web` - All jnss-web tasks
- `deploy` - Git clone/sync tasks
- `caddy` - Caddy configuration tasks
### Example Usage
```bash
# Full deployment
ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml
# Only update Caddy config (no git pull)
ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml --tags caddy
# Only sync files (skip Caddy reload)
ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml --tags deploy
```
## Initial Setup
### jnss-web Project Configuration
The following changes need to be made in the jnss-web project:
#### 1. Install Static Adapter
```bash
cd ~/dev/jnss-web
bun add -D @sveltejs/adapter-static
```
#### 2. Update svelte.config.js
```javascript
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined,
precompress: false,
strict: true
})
}
};
export default config;
```
#### 3. Create src/routes/+layout.js
```javascript
export const prerender = true;
```
#### 4. Update .gitignore
Comment out or remove the build directory from .gitignore:
```gitignore
# build/ <- Comment this out or remove it
```
#### 5. Create Deploy Branch
```bash
# Build the site first
bun run build
# Create and switch to deploy branch
git checkout -b deploy
# Remove source files (keep only build artifacts)
git rm -r src static *.config.js package.json bun.lock jsconfig.json .npmrc
# Keep only build/ and documentation
git add build/ README.md
# Commit
git commit -m "Initial deploy branch with build artifacts"
# Push to Gitea
git push -u origin deploy
# Switch back to main
git checkout main
```
## Troubleshooting
### Build Directory Not Found
**Error**: "Build directory not found in repository"
**Solution**: Ensure you pushed the build artifacts to the deploy branch:
```bash
git checkout deploy
ls -la build/ # Should exist
git log --oneline -1 # Verify latest commit
```
### Caddy Not Serving New Content
**Solution**:
```bash
# SSH to server
ssh root@arch-vps
# Check Caddy status
systemctl status caddy
# Reload Caddy manually
systemctl reload caddy
# Check logs
journalctl -u caddy -f
```
### Permission Issues
**Solution**:
```bash
# SSH to server
ssh root@arch-vps
# Fix ownership
chown -R caddy:caddy /var/www/jnss-web
# Verify
ls -la /var/www/jnss-web
```
### Git Clone Fails
**Error**: Unable to clone from git.jnss.me
**Solution**:
- Verify repository is public in Gitea
- Test clone manually: `git clone https://git.jnss.me/joakim/jnss-web.git /tmp/test`
- Check Gitea service status
## Rollback Procedure
### Option 1: Rollback Deploy Branch
```bash
# In jnss-web repository
git checkout deploy
# Find previous commit
git log --oneline
# Reset to previous commit
git reset --hard <commit-hash>
# Force push
git push -f origin deploy
# Re-run deployment
cd ~/rick-infra
ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml
```
### Option 2: Quick Server-Side Rollback
```bash
# SSH to server
ssh root@arch-vps
# Go to repo directory
cd /opt/jnss-web-repo
# Reset to previous commit
git reset --hard HEAD~1
# Re-sync
rsync -av --delete build/ /var/www/jnss-web/
# Reload Caddy
systemctl reload caddy
```
## Security Considerations
- Repository is public (contains only build artifacts)
- No secrets in build output
- Caddy serves only /var/www/jnss-web (no parent directory access)
- Security headers configured in Caddy
- HTTPS enforced via Caddy with Let's Encrypt
## Performance Optimizations
- Static assets cached for 1 year (immutable)
- HTML cached for 1 hour with revalidation
- Gzip compression enabled
- No server-side processing required
## Monitoring
### Check Deployment Status
```bash
# From control machine
ansible homelab -i inventory/hosts.yml -m shell -a "ls -lh /var/www/jnss-web"
```
### View Access Logs
```bash
ssh root@arch-vps 'tail -100 /var/log/caddy/jnss-web.log | jq .'
```
### Check Site Health
```bash
curl -I https://jnss.me
curl -I https://www.jnss.me # Should redirect to jnss.me
```
## Files Created
This deployment adds the following files to rick-infra:
- `playbooks/deploy-jnss-web.yml` - Main deployment playbook
- `playbooks/templates/jnss-web.caddy.j2` - Caddy configuration template
- `docs/jnss-web-deployment.md` - This documentation
And modifies:
- `roles/caddy/templates/Caddyfile.j2` - Removed default site section

View File

@@ -0,0 +1,311 @@
# Metrics Stack Deployment Guide
Complete guide to deploying the monitoring stack (VictoriaMetrics, Grafana, node_exporter) on rick-infra.
## Overview
The metrics stack provides:
- **System monitoring**: CPU, memory, disk, network via node_exporter
- **Time-series storage**: VictoriaMetrics (Prometheus-compatible, 7x less RAM)
- **Visualization**: Grafana with Authentik SSO integration
- **Access**: `https://metrics.jnss.me` with role-based permissions
## Architecture
```
User → metrics.jnss.me (HTTPS)
Caddy (Reverse Proxy)
Grafana (OAuth → Authentik for SSO)
VictoriaMetrics (Time-series DB)
node_exporter (System Metrics)
```
All services run on localhost only, following rick-infra security principles.
## Prerequisites
### 1. Caddy Deployed
```bash
ansible-playbook rick-infra.yml --tags caddy
```
### 2. Authentik Deployed
```bash
ansible-playbook rick-infra.yml --tags authentik
```
### 3. DNS Configuration
Ensure `metrics.jnss.me` points to arch-vps IP:
```bash
dig metrics.jnss.me # Should return 69.62.119.31
```
## Step 1: Configure Authentik OAuth Provider
### Create OAuth2/OIDC Provider
1. Login to Authentik at `https://auth.jnss.me`
2. Navigate to **Applications → Providers****Create**
3. Configure provider:
- **Name**: `Grafana`
- **Type**: `OAuth2/OpenID Provider`
- **Authentication flow**: `default-authentication-flow`
- **Authorization flow**: `default-provider-authorization-explicit-consent`
- **Client type**: `Confidential`
- **Client ID**: `grafana`
- **Client Secret**: Click **Generate** and **copy the secret**
- **Redirect URIs**: `https://metrics.jnss.me/login/generic_oauth`
- **Signing Key**: Select auto-generated key
- **Scopes**: `openid`, `profile`, `email`, `groups`
4. Click **Finish**
### Create Application
1. Navigate to **Applications****Create**
2. Configure application:
- **Name**: `Grafana`
- **Slug**: `grafana`
- **Provider**: Select `Grafana` provider created above
- **Launch URL**: `https://metrics.jnss.me`
3. Click **Create**
### Create Groups (Optional)
For role-based access control:
1. Navigate to **Directory → Groups****Create**
2. Create groups:
- **grafana-admins**: Full admin access to Grafana
- **grafana-editors**: Can create/edit dashboards
- All other users get Viewer access
3. Add users to groups as needed
## Step 2: Configure Vault Variables
Edit vault file:
```bash
ansible-vault edit host_vars/arch-vps/vault.yml
```
Add these variables:
```yaml
# Grafana admin password (for emergency local login)
vault_grafana_admin_password: "your-secure-admin-password"
# Grafana secret key (generate with: openssl rand -base64 32)
vault_grafana_secret_key: "your-random-32-char-secret-key"
# OAuth credentials from Authentik
vault_grafana_oauth_client_id: "grafana"
vault_grafana_oauth_client_secret: "paste-secret-from-authentik-here"
```
Save and close (`:wq` in vim).
## Step 3: Deploy Metrics Stack
Deploy all components:
```bash
ansible-playbook rick-infra.yml --tags metrics
```
This will:
1. Install and configure VictoriaMetrics
2. Install and configure node_exporter
3. Install and configure Grafana with OAuth
4. Deploy Caddy configuration for `metrics.jnss.me`
Expected output:
```
PLAY RECAP *******************************************************
arch-vps : ok=25 changed=15 unreachable=0 failed=0 skipped=0
```
## Step 4: Verify Deployment
### Check Services
SSH to arch-vps and verify services:
```bash
# Check all services are running
systemctl status victoriametrics grafana node_exporter
# Check service health
curl http://127.0.0.1:8428/health # VictoriaMetrics
curl http://127.0.0.1:9100/metrics # node_exporter
curl http://127.0.0.1:3000/api/health # Grafana
```
### Check HTTPS Access
```bash
curl -I https://metrics.jnss.me
# Should return 200 or 302 (redirect to Authentik)
```
### Check Metrics Collection
```bash
# Check VictoriaMetrics scrape targets
curl http://127.0.0.1:8428/api/v1/targets
# Should show node_exporter as "up"
```
## Step 5: Access Grafana
1. Navigate to `https://metrics.jnss.me`
2. Click **"Sign in with Authentik"**
3. Login with your Authentik credentials
4. You should be redirected to Grafana dashboard
First login will:
- Auto-create your Grafana user
- Assign role based on Authentik group membership
- Grant access to default organization
## Step 6: Verify Data Source
1. In Grafana, navigate to **Connections → Data sources**
2. Verify **VictoriaMetrics** is listed and default
3. Click on VictoriaMetrics → **Save & test**
4. Should show green "Data source is working" message
## Step 7: Create First Dashboard
### Option 1: Import Community Dashboard (Recommended)
1. Navigate to **Dashboards → Import**
2. Enter dashboard ID: `1860` (Node Exporter Full)
3. Click **Load**
4. Select **VictoriaMetrics** as data source
5. Click **Import**
You now have a comprehensive system monitoring dashboard!
### Option 2: Create Custom Dashboard
1. Navigate to **Dashboards → New → New Dashboard**
2. Click **Add visualization**
3. Select **VictoriaMetrics** data source
4. Enter PromQL query:
```promql
# CPU usage
100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
```
5. Click **Apply**
## Step 8: Configure Alerting (Optional)
Grafana supports alerting on metrics. Configure via **Alerting → Alert rules**.
Example alert for high CPU:
```promql
avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 < 20
```
## Troubleshooting
### OAuth Login Fails
**Symptom**: Redirect to Authentik, but returns error after login
**Solution**:
1. Verify redirect URI in Authentik matches exactly: `https://metrics.jnss.me/login/generic_oauth`
2. Check Grafana logs: `journalctl -u grafana -f`
3. Verify OAuth credentials in vault match Authentik
### No Metrics in Grafana
**Symptom**: Data source working, but no data in dashboards
**Solution**:
1. Check VictoriaMetrics targets: `curl http://127.0.0.1:8428/api/v1/targets`
2. Verify node_exporter is up: `systemctl status node_exporter`
3. Check time range in Grafana (top right) - try "Last 5 minutes"
### Can't Access metrics.jnss.me
**Symptom**: Connection timeout or SSL error
**Solution**:
1. Verify DNS: `dig metrics.jnss.me`
2. Check Caddy is running: `systemctl status caddy`
3. Check Caddy logs: `journalctl -u caddy -f`
4. Verify Caddy config loaded: `ls /etc/caddy/sites/grafana.caddy`
### Wrong Grafana Role
**Symptom**: User has wrong permissions (e.g., Viewer instead of Admin)
**Solution**:
1. Verify user is in correct Authentik group (`grafana-admins` or `grafana-editors`)
2. Logout of Grafana and login again
3. Check role mapping expression in `roles/metrics/defaults/main.yml`:
```yaml
grafana_oauth_role_attribute_path: "contains(groups, 'grafana-admins') && 'Admin' || contains(groups, 'grafana-editors') && 'Editor' || 'Viewer'"
```
## Next Steps
### Add More Hosts
To monitor additional hosts (e.g., mini-vps):
1. Deploy node_exporter to target host
2. Update VictoriaMetrics scrape config to include remote targets
3. Configure remote_write or federation
### Add Service Metrics
To monitor containerized services:
1. Expose `/metrics` endpoint in application (port 8080)
2. Add scrape config in `roles/metrics/templates/scrape.yml.j2`:
```yaml
- job_name: 'myservice'
static_configs:
- targets: ['127.0.0.1:8080']
```
3. Redeploy metrics role
### Set Up Alerting
1. Configure notification channels in Grafana (Email, Slack, etc.)
2. Create alert rules for critical metrics
3. Set up on-call rotation if needed
## Security Notes
- All metrics services run on localhost only
- Grafana is the only internet-facing component (via Caddy HTTPS)
- OAuth provides SSO with Authentik (no separate Grafana passwords)
- systemd hardening enabled on all services
- Default admin account should only be used for emergencies
## Resources
- **VictoriaMetrics Docs**: https://docs.victoriametrics.com/
- **Grafana Docs**: https://grafana.com/docs/
- **PromQL Guide**: https://prometheus.io/docs/prometheus/latest/querying/basics/
- **Dashboard Library**: https://grafana.com/grafana/dashboards/
- **Authentik OAuth**: https://goauthentik.io/docs/providers/oauth2/
## Support
For issues specific to rick-infra metrics deployment:
1. Check service logs: `journalctl -u <service> -f`
2. Review role README: `roles/metrics/README.md`
3. Verify vault variables are correctly set
4. Ensure Authentik OAuth provider is properly configured

View File

@@ -88,31 +88,42 @@ ssh root@your-vps "journalctl -u sshd | grep -i 'failed\|invalid'"
### Firewall Configuration ### Firewall Configuration
#### nftables Firewall Rules #### Modular nftables Architecture
Rick-infra uses a **modular firewall architecture** that enables self-contained service roles:
**Structure:**
```
/etc/nftables.conf Base infrastructure rules (SSH, HTTP, HTTPS)
/etc/nftables-load.conf Orchestration script (loads rules in order)
/etc/nftables.d/
├── 50-gitea.nft Service-specific rules (Gitea SSH port 2222)
└── 99-drop.nft Final drop rule (loaded last)
```
**Load Order:**
1. Base infrastructure rules (always allowed)
2. Service-specific rules (00-98 prefix)
3. Final drop rule (99-drop.nft)
**Example: Current Ruleset**
```bash ```bash
# Deployed firewall configuration
table inet filter { table inet filter {
chain input { chain input {
type filter hook input priority 0; policy drop; type filter hook input priority 0; policy drop;
# Allow loopback traffic # Base infrastructure rules
iifname "lo" accept iif "lo" accept
# Allow established connections
ct state established,related accept ct state established,related accept
tcp dport 22 ct state new accept
# Allow SSH (rate limited) tcp dport {80, 443} ct state new accept
tcp dport 22 ct state new limit rate 5/minute accept
# Allow HTTP/HTTPS
tcp dport {80, 443} accept
# Allow ICMP (rate limited)
icmp type echo-request limit rate 1/second accept icmp type echo-request limit rate 1/second accept
# Log dropped packets # Service-specific rules (loaded from /etc/nftables.d/)
log prefix "DROPPED: " drop tcp dport 2222 ct state new accept comment "Gitea SSH (Port 2222)"
# Final drop rule (99-drop.nft)
counter drop comment "Drop all other traffic"
} }
chain forward { chain forward {
@@ -129,15 +140,38 @@ table inet filter {
```bash ```bash
# Check firewall status # Check firewall status
ssh root@your-vps "nft list ruleset" nft list ruleset
# Monitor dropped connections # View service-specific rules
ssh root@your-vps "journalctl -k | grep DROPPED" ls -la /etc/nftables.d/
cat /etc/nftables.d/50-gitea.nft
# Temporary rule addition (emergency access) # Reload firewall (after manual changes)
ssh root@your-vps "nft add rule inet filter input tcp dport 8080 accept" systemctl restart nftables
# Test configuration syntax
nft -c -f /etc/nftables-load.conf
# Add service rule (example for future services)
# Create /etc/nftables.d/60-newservice.nft with your rules
echo 'add rule inet filter input tcp dport 8080 accept comment "New Service"' > /etc/nftables.d/60-newservice.nft
systemctl restart nftables
``` ```
#### Adding New Service Ports
When deploying new services that need firewall access:
1. Create rule file: `/etc/nftables.d/XX-servicename.nft` (XX = 00-98)
2. Add rule: `add rule inet filter input tcp dport PORT accept comment "Service Name"`
3. Restart nftables: `systemctl restart nftables`
**Naming Convention:**
- `00-19`: Infrastructure services
- `20-79`: Application services
- `80-98`: Custom/temporary rules
- `99`: Drop rule (reserved)
### Intrusion Detection (fail2ban) ### Intrusion Detection (fail2ban)
#### fail2ban Configuration #### fail2ban Configuration

View File

@@ -0,0 +1,367 @@
# Service Domain Configuration Standard
Standard pattern for domain configuration in rick-infra service roles.
## Architecture Philosophy
Rick-infra follows a **direct domain specification** pattern for service configuration:
```yaml
# Direct and explicit
service_domain: "subdomain.jnss.me"
# NOT this (complex and inflexible)
service_subdomain: "subdomain"
service_domain: "{{ caddy_domain }}"
service_full_domain: "{{ service_subdomain }}.{{ service_domain }}"
```
## Benefits
1. **Simplicity**: One variable instead of three
2. **Flexibility**: Can use any domain (subdomain, root, or completely different)
3. **Explicitness**: Clear what domain the service uses
4. **No Forced Inheritance**: Not tied to infrastructure `caddy_domain`
5. **Consistency**: All services follow the same pattern
---
## Standard Pattern
### Basic Service (Single Domain)
For services that only need one domain:
```yaml
# roles/service/defaults/main.yml
service_domain: "service.jnss.me"
# host_vars/host/main.yml (explicit override)
service_domain: "service.jnss.me"
```
**Examples:**
- Authentik: `authentik_domain: "auth.jnss.me"`
- Nextcloud: `nextcloud_domain: "cloud.jnss.me"`
### Advanced Service (Multiple Domains)
For services that need separate domains for different purposes:
```yaml
# roles/service/defaults/main.yml
service_http_domain: "service.jnss.me" # Web interface
service_api_domain: "api.jnss.me" # API endpoint
service_ssh_domain: "jnss.me" # SSH/CLI operations
# host_vars/host/main.yml (explicit override)
service_http_domain: "service.jnss.me"
service_api_domain: "api.jnss.me"
service_ssh_domain: "jnss.me"
```
**Example:**
- Gitea:
- `gitea_http_domain: "git.jnss.me"` (web interface)
- `gitea_ssh_domain: "jnss.me"` (Git operations)
---
## Usage in Templates
### Caddy Configuration
```jinja
# roles/service/templates/service.caddy.j2
{{ service_domain }} {
reverse_proxy 127.0.0.1:{{ service_port }}
}
```
### Application Configuration
```jinja
# roles/service/templates/service.conf.j2
[server]
DOMAIN = {{ service_domain }}
ROOT_URL = https://{{ service_domain }}/
```
### Task Display Messages
```yaml
# roles/service/tasks/main.yml
- name: Display service information
debug:
msg: |
🌐 Web Interface: https://{{ service_domain }}
📍 Access your service at the domain above
```
---
## Domain Selection Guidelines
### Use Root Domain When:
- Service is the primary purpose of the infrastructure
- You want cleaner URLs (e.g., SSH: `git@jnss.me` vs `git@git.jnss.me`)
- Industry standard uses root domain (e.g., GitHub uses `github.com` for SSH)
### Use Subdomain When:
- Service is one of many
- You want explicit service identification
- You need clear separation between services
### Use Different Domain When:
- Service needs to be on a different apex domain
- External service integration requires specific domain
- Multi-domain setup for geographical distribution
---
## Examples by Service Type
### Identity/Auth Service
```yaml
authentik_domain: "auth.jnss.me"
```
**Rationale**: Auth subdomain is an industry standard
### Storage Service
```yaml
nextcloud_domain: "cloud.jnss.me"
```
**Rationale**: "cloud" clearly indicates storage/sync service
### Git Service
```yaml
gitea_http_domain: "git.jnss.me" # Web UI
gitea_ssh_domain: "jnss.me" # SSH operations
```
**Rationale**:
- HTTP uses `git.` for clarity
- SSH uses root domain to avoid `git@git.jnss.me` redundancy
- Matches GitHub/GitLab pattern
### Monitoring Service
```yaml
grafana_domain: "monitor.jnss.me"
prometheus_domain: "metrics.jnss.me"
```
**Rationale**: Different subdomains for different monitoring tools
---
## Configuration Layers
### 1. Role Defaults (`roles/service/defaults/main.yml`)
Provide sensible defaults:
```yaml
# Option A: Use specific domain (explicit)
service_domain: "service.jnss.me"
# Option B: Use caddy_domain if it makes sense (flexible)
service_domain: "service.{{ caddy_domain | default('localhost') }}"
# Recommendation: Use Option A for clarity
```
### 2. Host Variables (`host_vars/hostname/main.yml`)
**Always explicitly set** in production:
```yaml
# =================================================================
# Service Configuration
# =================================================================
service_domain: "service.jnss.me"
```
**Why explicit?**
- Clear what domain is configured
- Easy to change without understanding defaults
- Easier to audit configuration
- Documentation in configuration itself
### 3. Group Variables (`group_vars/production/main.yml`)
For settings shared across production hosts:
```yaml
# Common production settings
service_enable_ssl: true
service_require_auth: true
# Generally avoid setting domains in group_vars
# (domains are usually host-specific)
```
---
## Anti-Patterns to Avoid
### ❌ Subdomain Composition
```yaml
# DON'T DO THIS
service_subdomain: "service"
service_domain: "{{ caddy_domain }}"
service_full_domain: "{{ service_subdomain }}.{{ service_domain }}"
```
**Problems:**
- Complex (3 variables for 1 domain)
- Inflexible (can't use root or different domains)
- Forces inheritance from infrastructure variable
- Inconsistent with other services
### ❌ Implicit Inheritance
```yaml
# DON'T DO THIS
service_domain: "{{ caddy_domain }}"
```
**Problems:**
- Not explicit what domain is used
- Harder to change
- Hides actual configuration
- Requires understanding of infrastructure variables
### ❌ Mixed Patterns
```yaml
# DON'T DO THIS
authentik_domain: "auth.jnss.me" # Direct
nextcloud_subdomain: "cloud" # Composition
service_domain: "{{ caddy_domain }}" # Inheritance
```
**Problems:**
- Inconsistent
- Confusing for maintainers
- Different patterns for same purpose
---
## Migration from Old Pattern
If you have services using the old subdomain composition pattern:
### Step 1: Identify Current Variables
```yaml
# Old pattern
service_subdomain: "service"
service_domain: "{{ caddy_domain }}"
service_full_domain: "{{ service_subdomain }}.{{ service_domain }}"
```
### Step 2: Replace with Direct Domain
```yaml
# New pattern
service_domain: "service.jnss.me"
```
### Step 3: Update Template References
```jinja
# Old
{{ service_full_domain }}
# New
{{ service_domain }}
```
### Step 4: Remove Unused Variables
Delete `service_subdomain` and `service_full_domain` from defaults.
### Step 5: Add Explicit Host Configuration
```yaml
# host_vars/arch-vps/main.yml
service_domain: "service.jnss.me"
```
---
## Testing Domain Configuration
### Verify Caddy Configuration
```bash
# Check generated Caddy config
cat /etc/caddy/sites-enabled/service.caddy
# Test Caddy configuration syntax
caddy validate --config /etc/caddy/Caddyfile
# Check TLS certificate
curl -I https://service.jnss.me
```
### Verify Application Configuration
```bash
# Check service configuration
cat /etc/service/config.ini | grep -i domain
# Test service accessibility
curl https://service.jnss.me
```
### Verify DNS Resolution
```bash
# Check DNS resolution
dig service.jnss.me
# Test connectivity
nc -zv service.jnss.me 443
```
---
## Checklist for New Services
When creating a new service role:
- [ ] Use direct domain specification (not subdomain composition)
- [ ] Define domain(s) in `roles/service/defaults/main.yml`
- [ ] Add explicit domain(s) to host_vars
- [ ] Update all templates to use domain variable(s)
- [ ] Document domain configuration in role README
- [ ] Follow naming convention: `service_domain` or `service_[type]_domain`
- [ ] Test with different domain configurations
---
## Summary
**Standard Pattern:**
```yaml
# Defaults: Provide reasonable default
service_domain: "service.jnss.me"
# Host vars: Always explicit in production
service_domain: "service.jnss.me"
# Templates: Use variable directly
{{ service_domain }}
```
**Key Principles:**
1. Direct and explicit
2. One variable per domain
3. No forced inheritance
4. Consistent across all services
5. Flexible for any domain pattern
---
**Rick-Infra Domain Configuration Standard**
Simple, flexible, and consistent domain configuration for all services.

View File

@@ -1,39 +1,50 @@
# Sigvild Gallery Deployment Guide # Sigvild Gallery Deployment Guide
## Overview
Sigvild Wedding Gallery is deployed on **mini-vps** (production environment) for high uptime and reliability. The gallery uses PocketBase API backend with SvelteKit frontend.
**Production Host**: mini-vps (72.62.91.251)
**Domains**: sigvild.no (frontend), api.sigvild.no (API)
## Quick Start ## Quick Start
Deploy the complete Sigvild Wedding Gallery with PocketBase API and SvelteKit frontend. Deploy Sigvild Gallery to production:
```bash
ansible-playbook playbooks/production.yml
```
## Prerequisites Setup ## Prerequisites Setup
### 1. Vault Password Configuration ### 1. Vault Password Configuration
Create encrypted passwords for the gallery authentication: Encrypted passwords are stored in `group_vars/production/vault.yml`:
```bash ```bash
# Create vault passwords (run from rick-infra directory) # Edit production vault (run from rick-infra directory)
ansible-vault encrypt_string 'your-host-password-here' --name 'vault_sigvild_host_password' ansible-vault edit group_vars/production/vault.yml
ansible-vault encrypt_string 'your-guest-password-here' --name 'vault_sigvild_guest_password'
``` ```
Add the encrypted strings to `host_vars/arch-vps/main.yml`: Add these variables:
```yaml ```yaml
# Add to host_vars/arch-vps/main.yml # Production vault variables
vault_sigvild_host_password: !vault | vault_cloudflare_api_token: "your-cloudflare-token"
$ANSIBLE_VAULT;1.1;AES256 vault_caddy_tls_email: "admin@example.com"
66386439653765386... vault_sigvild_host_password: "host-user-password"
vault_sigvild_guest_password: "guest-user-password"
vault_sigvild_guest_password: !vault | vault_pb_su_email: "admin@sigvild.no"
$ANSIBLE_VAULT;1.1;AES256 vault_pb_su_password: "admin-password"
33663065383834313...
``` ```
**Note**: Use `ansible-vault encrypt group_vars/production/vault.yml` after editing.
### 2. DNS Configuration ### 2. DNS Configuration
Ensure these domains point to your server: Point these domains to **mini-vps** (72.62.91.251):
- `sigvild.no` → Frontend static site - `sigvild.no` A record → 72.62.91.251
- `api.sigvild.no` → API backend proxy - `api.sigvild.no` A record → 72.62.91.251
### 3. Project Structure ### 3. Project Structure
@@ -50,70 +61,92 @@ Ensure the sigvild-gallery project is adjacent to rick-infra:
## Deployment Commands ## Deployment Commands
### Full Infrastructure + Gallery ### Production Deployment
Deploy everything including Sigvild Gallery: Deploy to production environment (mini-vps):
```bash ```bash
ansible-playbook site.yml # Deploy complete production stack (Caddy + Sigvild Gallery)
``` ansible-playbook playbooks/production.yml
### Gallery Only # Or deploy everything with production limit
ansible-playbook site.yml -l production
Deploy just the Sigvild Gallery service:
```bash
ansible-playbook playbooks/deploy-sigvild.yml
``` ```
### Selective Updates ### Selective Updates
Update specific components: Update specific components using tags:
```bash ```bash
# Frontend only (quick static file updates) # Frontend only (quick static file updates)
ansible-playbook site.yml --tags="frontend" ansible-playbook playbooks/production.yml --tags="frontend"
# Backend only (API service updates) # Backend only (API service updates)
ansible-playbook site.yml --tags="backend" ansible-playbook playbooks/production.yml --tags="backend"
# Caddy configuration only # Caddy configuration only
ansible-playbook site.yml --tags="caddy" ansible-playbook playbooks/production.yml --tags="caddy"
# Just build process (development) # Just build process (development)
ansible-playbook site.yml --tags="build" ansible-playbook playbooks/production.yml --tags="build"
``` ```
### Backup and Restore
Create backups before major changes:
```bash
# Create backup (works on any host running sigvild-gallery)
ansible-playbook playbooks/backup-sigvild.yml -l mini-vps
# Backup is saved to: ~/sigvild-gallery-backup/
```
**Automatic Restore**: When deploying to a fresh server, the role automatically detects and restores from the latest backup if available.
## Architecture Overview ## Architecture Overview
**Production Environment**: mini-vps (72.62.91.251)
``` ```
Internet Internet
Caddy (Auto HTTPS) Cloudflare DNS → mini-vps
Caddy (Auto HTTPS with DNS Challenge)
├── sigvild.no → /var/www/sigvild-gallery/ (Static Files) ├── sigvild.no → /var/www/sigvild-gallery/ (Static Files)
└── api.sigvild.no → localhost:8090 (PocketBase API) └── api.sigvild.no → localhost:8090 (PocketBase API)
Go Binary (sigvild-gallery-server) Go Binary (/opt/sigvild-gallery/sigvild-gallery)
SQLite Database + File Storage SQLite Database (/opt/sigvild-gallery/pb_data/)
└── File Storage (wedding photos)
``` ```
**Key Features**:
- Automatic HTTPS with Let's Encrypt
- Cloudflare DNS challenge for certificate validation
- Security headers and CORS protection
- SystemD service management
- Automatic backup/restore capability
## Service Management ## Service Management
### Status Checks ### Status Checks
```bash ```bash
# Gallery API service # Check services on mini-vps
systemctl status sigvild-gallery ansible mini-vps -a "systemctl status sigvild-gallery"
ansible mini-vps -a "systemctl status caddy"
# Caddy web server
systemctl status caddy
# View gallery logs # View gallery logs
journalctl -u sigvild-gallery -f ansible mini-vps -a "journalctl -u sigvild-gallery -n 50 --no-pager"
# View Caddy logs # View Caddy logs
journalctl -u caddy -f ansible mini-vps -a "journalctl -u caddy -n 20 --no-pager"
# Check data directory
ansible mini-vps -a "ls -lh /opt/sigvild-gallery/pb_data/"
``` ```
### Manual Operations ### Manual Operations
@@ -216,23 +249,30 @@ journalctl -u caddy | grep -i "acme\|certificate"
- **CORS restrictions**: API access limited to frontend domain - **CORS restrictions**: API access limited to frontend domain
- **Rate limiting**: API endpoint protection - **Rate limiting**: API endpoint protection
## File Locations ## File Locations (on mini-vps)
### Application Files ### Application Files
- **Binary**: `/opt/sigvild-gallery/sigvild-gallery-server` - **Binary**: `/opt/sigvild-gallery/sigvild-gallery`
- **Database**: `/opt/sigvild-gallery/data/data.db` - **Database**: `/opt/sigvild-gallery/pb_data/data.db`
- **File uploads**: `/opt/sigvild-gallery/data/storage/` - **File uploads**: `/opt/sigvild-gallery/pb_data/storage/`
- **Frontend**: `/var/www/sigvild-gallery/` - **Frontend**: `/var/www/sigvild-gallery/`
- **User/Group**: `sigvild:sigvild`
### Configuration Files ### Configuration Files
- **Service**: `/etc/systemd/system/sigvild-gallery.service` - **Service**: `/etc/systemd/system/sigvild-gallery.service`
- **Caddy frontend**: `/etc/caddy/sites-enabled/sigvild-frontend.caddy` - **Caddy frontend**: `/etc/caddy/sites-enabled/sigvild-frontend.caddy`
- **Caddy API**: `/etc/caddy/sites-enabled/sigvild-api.caddy` - **Caddy API**: `/etc/caddy/sites-enabled/sigvild-api.caddy`
### Local Files (on control machine)
- **Configuration**: `group_vars/production/main.yml`
- **Secrets**: `group_vars/production/vault.yml` (encrypted)
- **Backups**: `~/sigvild-gallery-backup/`
- **Source code**: `~/sigvild-gallery/`
### Log Files ### Log Files
- **Service logs**: `journalctl -u sigvild-gallery` - **Service logs**: `journalctl -u sigvild-gallery`
- **Caddy logs**: `journalctl -u caddy` - **Caddy logs**: `journalctl -u caddy`
- **Access logs**: `/var/log/caddy/sigvild-*.log` - **Access logs**: `/var/log/caddy/access.log`
## Next Steps After Deployment ## Next Steps After Deployment
@@ -248,15 +288,28 @@ For ongoing development:
```bash ```bash
# 1. Make changes to sigvild-gallery project # 1. Make changes to sigvild-gallery project
cd ../sigvild-gallery cd ~/sigvild-gallery
# 2. Test locally # 2. Test locally
go run . serve & go run . serve &
cd sigvild-kit && npm run dev cd sigvild-kit && npm run dev
# 3. Deploy updates # 3. Deploy updates to production
cd ../rick-infra cd ~/rick-infra
ansible-playbook site.yml --tags="sigvild" ansible-playbook playbooks/production.yml --tags="sigvild"
``` ```
The deployment system builds locally and transfers assets, so you don't need build tools on the server. **Build Process**:
- Backend: Built locally with `GOOS=linux GOARCH=amd64 go build`
- Frontend: Built locally with `npm run build` in sigvild-kit/
- Assets transferred to mini-vps via Ansible
- No build tools required on the server
## Migration History
**December 2025**: Migrated from arch-vps (homelab) to mini-vps (production)
- **Reason**: Client project requiring higher uptime reliability
- **Method**: Backup from arch-vps, automatic restore to mini-vps
- **Downtime**: ~5 minutes during DNS propagation
- **Previous host**: arch-vps (69.62.119.31)
- **Current host**: mini-vps (72.62.91.251)

View File

@@ -0,0 +1,307 @@
# Vaultwarden SSO Feature Status and Configuration
**Document Date:** December 21, 2025
**Last Updated:** December 21, 2025
**Status:** SSO Configured, Waiting for Stable Release
---
## Executive Summary
Vaultwarden has been successfully deployed with **SSO integration pre-configured** for Authentik. However, SSO functionality is currently **only available in testing images** and not yet in the stable release. This document explains the current state, our decision to wait for stable, and how to activate SSO when it becomes available.
## Current Deployment Status
### What's Working
- ✅ Vaultwarden deployed successfully at `https://vault.jnss.me`
- ✅ PostgreSQL backend via Unix socket
- ✅ Admin panel accessible and working
- ✅ Email/password authentication working
- ✅ SMTP notifications configured
- ✅ All SSO environment variables correctly configured
- ✅ Authentik OAuth2 provider created and ready
### What's Not Working (By Design)
- ❌ SSO login option not appearing on login page
- ❌ "Use single sign-on" button missing
**Reason:** Using stable image (`vaultwarden/server:latest` v1.34.3) which does not include SSO code.
## Investigation Summary (Dec 21, 2025)
### Problem Reported
User deployed Vaultwarden with `vaultwarden_sso_enabled: true` and configured Authentik integration following official guides, but no SSO option appeared on the login page.
### Root Cause Identified
After investigation including:
- Service status check (healthy, running normally)
- Environment variable verification (all SSO vars present and correct)
- Configuration review (matches Authentik integration guide perfectly)
- API endpoint inspection (`/api/config` returns `"sso":""`)
- Official documentation review
**Finding:** SSO feature is only compiled into `vaultwarden/server:testing` images, not stable releases.
### Evidence
From [Vaultwarden Official Wiki](https://github.com/dani-garcia/vaultwarden/wiki):
> **Testing features**
>
> Features available in the `testing` docker image:
> - Single Sign-On (SSO), see [Documentation](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect)
From [SSO Documentation Page](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect):
> ⚠️ **Important**
>
> ‼️ ‼️ ‼️
> SSO is currently only available in the `:testing` tagged images!
> The current stable `v1.34.3` **does not** contain the SSO feature.
> ‼️ ‼️ ‼️
### API Response Analysis
```bash
# API config endpoint shows SSO not available
curl -s http://127.0.0.1:8080/api/config | grep -o '"sso":"[^"]*"'
# Returns: "sso":"" ← Empty = feature not compiled in
# Environment variables are set correctly
podman exec vaultwarden env | grep -i sso
SSO_ENABLED=true
SSO_ONLY=false
SSO_CLIENT_ID=DDOQXdwFn6pi4FtSvo7PK5b63pRzyD552xapTZGr
SSO_CLIENT_SECRET=02D308Sle2w2NPsi7UaXb3bvKKK4punFDT2LiVqKpzvEFqgPpyLysA8Z5yS4g8t4LYmsI9txLE02l5MtWP5R2RBavLhYHjNFHcwEmvYB94bOJw45YmgiGePaW4NHKcfY
SSO_AUTHORITY=https://auth.jnss.me/application/o/vaultwarden/
SSO_SCOPES="openid email profile offline_access"
# ... etc (all correct)
```
**Conclusion:** Environment configured correctly, feature simply not available in stable release.
## Decision: Wait for Stable Release
### Rationale
**Why NOT switch to testing image:**
1. **Production stability** - This is a password manager handling sensitive credentials
2. **Testing images are volatile** - Frequent updates, potential bugs
3. **No ETA for stable** - SSO marked as "testing feature" indefinitely
4. **Current auth works fine** - Email/password login is secure and functional
5. **Configuration is ready** - When SSO reaches stable, it will work immediately
**Why keep SSO configured:**
1. **Future-ready** - No additional work needed when SSO stabilizes
2. **No harm** - Environment variables are ignored by stable image
3. **Documentation** - Clear record of SSO setup for future reference
4. **Authentik provider ready** - Already created and configured
### Alternatives Considered
| Option | Pros | Cons | Decision |
|--------|------|------|----------|
| Use `testing` image | SSO available now | Unstable, potential data loss, frequent breaking changes | ❌ Rejected |
| Wait for stable | Stable, reliable, secure | No SSO until unknown future date | ✅ **Selected** |
| Remove SSO config | Cleaner config | Requires reconfiguration later | ❌ Rejected |
| Dual deployment | Test SSO separately | Resource waste, complexity | ❌ Rejected |
## Current Configuration
### Ansible Role Variables
Location: `roles/vaultwarden/defaults/main.yml`
```yaml
# Container version (stable, no SSO)
vaultwarden_version: "latest"
# SSO enabled (ready for when feature reaches stable)
vaultwarden_sso_enabled: true
vaultwarden_sso_only: false
vaultwarden_sso_client_id: "{{ vault_vaultwarden_sso_client_id }}"
vaultwarden_sso_client_secret: "{{ vault_vaultwarden_sso_client_secret }}"
vaultwarden_sso_authority: "https://auth.jnss.me/application/o/vaultwarden/"
vaultwarden_sso_scopes: "openid email profile offline_access"
vaultwarden_sso_signups_match_email: true
vaultwarden_sso_allow_unknown_email_verification: false
vaultwarden_sso_client_cache_expiration: 0
```
### Vault Variables (Encrypted)
Location: `group_vars/homelab/vault.yml`
```yaml
# SSO credentials from Authentik
vault_vaultwarden_sso_client_id: "DDOQXdwFn6pi4FtSvo7PK5b63pRzyD552xapTZGr"
vault_vaultwarden_sso_client_secret: "02D308Sle2w2NPsi7UaXb3bvKKK4punFDT2LiVqKpzvEFqgPpyLysA8Z5yS4g8t4LYmsI9txLE02l5MtWP5R2RBavLhYHjNFHcwEmvYB94bOJw45YmgiGePaW4NHKcfY"
```
### Authentik Provider Configuration
**Provider Details:**
- **Name:** Vaultwarden
- **Type:** OAuth2/OpenID Connect Provider
- **Client Type:** Confidential
- **Client ID:** `DDOQXdwFn6pi4FtSvo7PK5b63pRzyD552xapTZGr`
- **Application Slug:** `vaultwarden`
- **Redirect URI:** `https://vault.jnss.me/identity/connect/oidc-signin`
**Scopes Configured:**
-`authentik default OAuth Mapping: OpenID 'openid'`
-`authentik default OAuth Mapping: OpenID 'email'`
-`authentik default OAuth Mapping: OpenID 'profile'`
-`authentik default OAuth Mapping: OpenID 'offline_access'`
**Token Settings:**
- Access token validity: > 5 minutes
- Refresh token enabled via `offline_access` scope
**Authority URL:** `https://auth.jnss.me/application/o/vaultwarden/`
### Deployed Environment (VPS)
```bash
# Deployed environment file
Location: /opt/vaultwarden/.env
# All SSO variables present and correct
SSO_ENABLED=true
SSO_AUTHORITY=https://auth.jnss.me/application/o/vaultwarden/
SSO_SCOPES="openid email profile offline_access"
# ... etc
```
## How to Activate SSO (Future)
When Vaultwarden SSO reaches stable release:
### Automatic Activation (Recommended)
1. **Monitor Vaultwarden releases** for SSO in stable:
- Watch: https://github.com/dani-garcia/vaultwarden/releases
- Look for: SSO feature in stable image changelog
2. **Update container** (standard maintenance):
```bash
ansible-playbook rick-infra.yml --tags vaultwarden --ask-vault-pass
```
3. **Verify SSO is available**:
```bash
ssh root@69.62.119.31 "curl -s http://127.0.0.1:8080/api/config | grep sso"
# Should return: "sso":"https://vault.jnss.me" or similar
```
4. **Test SSO**:
- Navigate to: https://vault.jnss.me
- Log out if logged in
- Enter verified email address
- Click "Use single sign-on" button
- Should redirect to Authentik login
**No configuration changes needed** - Everything is already set up correctly.
### Manual Testing (Use Testing Image)
If you want to test SSO before stable release:
1. **Backup current deployment**:
```bash
ansible-playbook playbooks/backup-vaultwarden.yml # Create if needed
```
2. **Change to testing image** in `roles/vaultwarden/defaults/main.yml`:
```yaml
vaultwarden_version: "testing"
```
3. **Deploy**:
```bash
ansible-playbook rick-infra.yml --tags vaultwarden --ask-vault-pass
```
4. **Test SSO** (same as above)
5. **Revert to stable** when testing complete:
```yaml
vaultwarden_version: "latest"
```
**Warning:** Testing images may contain bugs, data corruption risks, or breaking changes. Not recommended for production password manager.
## Verification Commands
### Check Current Image Version
```bash
ssh root@69.62.119.31 "podman inspect vaultwarden --format '{{.ImageName}}'"
# Expected: docker.io/vaultwarden/server:latest
```
### Check SSO API Status
```bash
ssh root@69.62.119.31 "curl -s http://127.0.0.1:8080/api/config | grep -o '\"sso\":\"[^\"]*\"'"
# Current: "sso":"" (empty = not available)
# Future: "sso":"https://vault.jnss.me" (URL = available)
```
### Check SSO Environment Variables
```bash
ssh root@69.62.119.31 "podman exec vaultwarden env | grep -i sso | sort"
# Should show all SSO_* variables configured correctly
```
### Check Vaultwarden Version
```bash
ssh root@69.62.119.31 "podman exec vaultwarden /vaultwarden --version"
# Current: Vaultwarden 1.34.3
```
## Documentation Updates
The following files have been updated to document this finding:
1. **`roles/vaultwarden/README.md`**
- Added warning banner in SSO configuration section
- Updated troubleshooting section with SSO status checks
- Documented stable vs testing image behavior
2. **`roles/vaultwarden/VAULT_VARIABLES.md`**
- Added SSO feature status warning
- Documented that credentials are ready but inactive
3. **`roles/vaultwarden/defaults/main.yml`**
- Added comments explaining SSO availability
4. **`docs/vaultwarden-sso-status.md`** (this document)
- Complete investigation findings
- Configuration reference
- Activation procedures
## Timeline
- **2025-07-30:** Vaultwarden v1.34.3 released (current stable)
- **2025-12-21:** Vaultwarden deployed with SSO pre-configured
- **2025-12-21:** Investigation completed, SSO status documented
- **TBD:** SSO feature reaches stable release (no ETA)
- **Future:** Automatic SSO activation on next deployment
## Related Documentation
- [Vaultwarden Official Wiki](https://github.com/dani-garcia/vaultwarden/wiki)
- [SSO Documentation](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect)
- [Authentik Integration Guide](https://integrations.goauthentik.io/security/vaultwarden/)
- [Vaultwarden Testing Features](https://github.com/dani-garcia/vaultwarden/wiki#testing-features)
## Contact
For questions about this deployment:
- Infrastructure repo: `/home/fitz/rick-infra`
- Role location: `roles/vaultwarden/`
- Service: `vault.jnss.me`
- Host: `arch-vps` (69.62.119.31)
---
**Status:** SSO configured and ready, waiting for upstream stable release. No action required.

View File

@@ -0,0 +1,71 @@
---
# =================================================================
# Production Configuration for mini-vps (Client Projects)
# =================================================================
# This host runs production services requiring high uptime
# Currently hosting: Sigvild Gallery, Devigo
# =================================================================
# TLS Configuration - Production Setup
# =================================================================
caddy_tls_enabled: true
caddy_domain: "health.sigvild.no"
caddy_tls_email: "{{ vault_caddy_tls_email }}"
# DNS Challenge Configuration (Cloudflare)
caddy_dns_provider: "cloudflare"
cloudflare_api_token: "{{ vault_cloudflare_api_token }}"
# Production Let's Encrypt CA
caddy_acme_ca: "https://acme-v02.api.letsencrypt.org/directory"
# =================================================================
# API Service Registration Configuration
# =================================================================
# Services now self-register using Caddy's admin API
caddy_api_enabled: true
caddy_server_name: "main"
# =================================================================
# Sigvild Gallery Configuration
# =================================================================
sigvild_gallery_frontend_domain: "sigvild.no"
sigvild_gallery_api_domain: "api.sigvild.no"
sigvild_gallery_local_project_path: "{{ lookup('env', 'HOME') }}/sigvild-gallery/"
# Backup configuration
sigvild_gallery_backup_enabled: true
sigvild_gallery_backup_local_path: "{{ lookup('env', 'HOME') }}/sigvild-gallery-backup/"
# Vault-encrypted passwords (create with ansible-vault)
sigvild_gallery_pb_su_email: "{{ vault_pb_su_email}}"
sigvild_gallery_pb_su_password: "{{ vault_pb_su_password}}"
sigvild_gallery_host_password: "{{ vault_sigvild_host_password }}"
sigvild_gallery_guest_password: "{{ vault_sigvild_guest_password }}"
# =================================================================
# Devigo Configuration (Docker-based deployment)
# =================================================================
devigo_domain: "devigo.no"
devigo_www_domain: "www.devigo.no"
devigo_primary_domain: "devigo.no" # Apex is primary
devigo_docker_dir: "/opt/devigo"
devigo_ghcr_image: "ghcr.io/jnschaffer/rustan:prod"
github_username: "{{ vault_github_username }}"
github_token: "{{ vault_github_token }}"
# Decap OAuth (integrated service)
devigo_oauth_domain: "decap.jnss.me"
devigo_oauth_client_id: "{{ vault_devigo_oauth_client_id }}"
devigo_oauth_client_secret: "{{ vault_devigo_oauth_client_secret }}"
devigo_oauth_trusted_origins:
- "https://devigo.no"
- "https://www.devigo.no"
# =================================================================
# Security & Logging
# =================================================================
caddy_log_level: "INFO"
caddy_log_format: "json"
caddy_systemd_security: true

View File

@@ -24,24 +24,6 @@ caddy_acme_ca: "https://acme-v02.api.letsencrypt.org/directory"
caddy_api_enabled: true caddy_api_enabled: true
caddy_server_name: "main" caddy_server_name: "main"
# =================================================================
# Sigvild Gallery Configuration
# =================================================================
sigvild_gallery_frontend_domain: "sigvild.no"
sigvild_gallery_api_domain: "api.sigvild.no"
sigvild_gallery_local_project_path: "~/sigvild-gallery/"
# Backup configuration
sigvild_gallery_backup_enabled: true
sigvild_gallery_backup_local_path: "~/sigvild-gallery-backup/"
# Vault-encrypted passwords (create with ansible-vault)
sigvild_gallery_pb_su_email: "{{ vault_pb_su_email}}"
sigvild_gallery_pb_su_password: "{{ vault_pb_su_password}}"
sigvild_gallery_host_password: "{{ vault_sigvild_host_password }}"
sigvild_gallery_guest_password: "{{ vault_sigvild_guest_password }}"
# ================================================================= # =================================================================
# Authentik Configuration # Authentik Configuration
# ================================================================= # =================================================================
@@ -83,13 +65,20 @@ nextcloud_db_password: "{{ vault_nextcloud_db_password }}"
nextcloud_valkey_db: 2 # Authentik uses 1 nextcloud_valkey_db: 2 # Authentik uses 1
# Admin configuration # Admin configuration
nextcloud_admin_user: "admin" nextcloud_admin_user: "joakim"
nextcloud_admin_email: "joakim@jnss.me"
nextcloud_admin_password: "{{ vault_nextcloud_admin_password }}" nextcloud_admin_password: "{{ vault_nextcloud_admin_password }}"
# Service configuration # Service configuration
nextcloud_service_enabled: true nextcloud_service_enabled: true
nextcloud_service_state: "started" nextcloud_service_state: "started"
# =================================================================
# Gitea Configuration
# =================================================================
gitea_http_domain: "git.jnss.me"
gitea_ssh_domain: "jnss.me"
# ================================================================= # =================================================================
# Security & Logging # Security & Logging
# ================================================================= # =================================================================

View File

View File

@@ -1,11 +1,15 @@
--- ---
all: homelab:
children:
production:
hosts: hosts:
arch-vps: arch-vps:
ansible_host: 69.62.119.31 ansible_host: 69.62.119.31
ansible_user: root ansible_user: root
vars:
ansible_python_interpreter: /usr/bin/python3
production:
hosts:
mini-vps:
ansible_host: 72.62.91.251
ansible_user: root
vars: vars:
ansible_python_interpreter: /usr/bin/python3 ansible_python_interpreter: /usr/bin/python3

31
now-what.md Normal file
View File

@@ -0,0 +1,31 @@
# Now what?
- [x] Redeploy on clean VPS to test playbook
- [x] Must set up mini-vps for sigvild and devigo
- [ ] What gets served on jnss.me?
- [ ] Backups
- [x] Titan email provider support. For smtp access to hello@jnss.me
- [ ] Vaultvarden
- [ ] Configure and set up Nextcloud
- [ ] OAuth
- [ ] Settings
- [ ] Contacts and calendars
- [ ] Storage bucket integration?
- [x] SMTP setup for email sending
- [x] Gitea
- [x] SSH passthrough setup
- [x] Figure out how to disable registration and local password
- [x] SMTP setup for email sending
- [ ] Authentik Invitations for users?
- [ ] Sail the high seas
- [ ] Set up Jellyfin
- [ ] Set up *arr applications
- [ ] "Blog post"

View File

@@ -0,0 +1,24 @@
---
# Sigvild Gallery Data Backup Playbook
#
# This playbook creates a backup of the Sigvild Gallery data including:
# - PocketBase SQLite database (data.db, auxiliary.db)
# - All uploaded wedding photos and media files
# - PocketBase logs and system state
#
# Usage:
# ansible-playbook playbooks/backup-sigvild.yml -l arch-vps
# ansible-playbook playbooks/backup-sigvild.yml -l mini-vps
#
# Backup location: ~/sigvild-gallery-backup/sigvild-gallery-backup-YYYYMMDDTHHMMSS.tar.gz
- name: Backup Sigvild Gallery Data
hosts: all
become: true
gather_facts: true
tasks:
- name: Run backup tasks from sigvild-gallery role
include_role:
name: sigvild-gallery
tasks_from: backup.yml

View File

@@ -0,0 +1,133 @@
---
# ================================================================
# jnss-web Static Site Deployment Playbook
# ================================================================
# Deploys the jnss-web SvelteKit static site to jnss.me
#
# Usage:
# ansible-playbook -i inventory/hosts.yml playbooks/deploy-jnss-web.yml
#
# This playbook:
# - Clones the jnss-web repository (deploy branch) to a temp directory
# - Syncs build artifacts to /var/www/jnss-web
# - Deploys Caddy configuration for jnss.me with www redirect
# - Reloads Caddy to serve the new site
# ================================================================
- name: Deploy jnss-web static site
hosts: homelab
become: true
vars:
# Git repository configuration
jnss_web_repo_url: "https://git.jnss.me/joakim/jnss-web.git"
jnss_web_branch: "deploy"
# Server paths
jnss_web_root: "/var/www/jnss-web"
# Domain configuration
jnss_web_domain: "jnss.me"
# Caddy configuration
caddy_user: "caddy"
caddy_sites_enabled_dir: "/etc/caddy/sites-enabled"
tasks:
# ============================================================
# Git Repository Management
# ============================================================
- name: Create temporary directory for git clone
tempfile:
state: directory
suffix: -jnss-web
register: temp_clone_dir
tags: [jnss-web, deploy]
- name: Clone jnss-web repository to temp directory
git:
repo: "{{ jnss_web_repo_url }}"
dest: "{{ temp_clone_dir.path }}"
version: "{{ jnss_web_branch }}"
depth: 1
tags: [jnss-web, deploy]
- name: Verify build directory exists in repository
stat:
path: "{{ temp_clone_dir.path }}/index.html"
register: build_dir
tags: [jnss-web, deploy]
- name: Fail if index.html not found
fail:
msg: "Build index.html not found in repository root. Ensure the deploy branch contains the built artifacts."
when: not build_dir.stat.exists
tags: [jnss-web, deploy]
# ============================================================
# Web Root Deployment
# ============================================================
- name: Remove old web root
file:
path: "{{ jnss_web_root }}"
state: absent
tags: [jnss-web, deploy]
- name: Create fresh web root directory
file:
path: "{{ jnss_web_root }}"
state: directory
owner: "{{ caddy_user }}"
group: "{{ caddy_user }}"
mode: '0755'
tags: [jnss-web, deploy]
- name: Copy build files to web root
copy:
src: "{{ temp_clone_dir.path }}/"
dest: "{{ jnss_web_root }}/"
owner: "{{ caddy_user }}"
group: "{{ caddy_user }}"
mode: '0755'
remote_src: true
tags: [jnss-web, deploy]
- name: Clean up temporary clone directory
file:
path: "{{ temp_clone_dir.path }}"
state: absent
tags: [jnss-web, deploy]
# ============================================================
# Caddy Configuration
# ============================================================
- name: Deploy Caddy configuration for jnss-web
template:
src: templates/jnss-web.caddy.j2
dest: "{{ caddy_sites_enabled_dir }}/jnss-web.caddy"
owner: root
group: "{{ caddy_user }}"
mode: '0644'
notify: reload caddy
tags: [jnss-web, caddy]
- name: Validate Caddy configuration
command: caddy validate --config /etc/caddy/Caddyfile
register: caddy_validate
changed_when: false
tags: [jnss-web, caddy]
- name: Display Caddy validation result
debug:
msg: "Caddy configuration is valid"
when: caddy_validate.rc == 0
tags: [jnss-web, caddy]
handlers:
- name: reload caddy
systemd:
name: caddy
state: reloaded

View File

@@ -0,0 +1,203 @@
---
# =================================================================
# Nextcloud Removal Playbook
# =================================================================
# Rick-Infra - Clean removal of Nextcloud installation
#
# This playbook removes all Nextcloud components:
# - Systemd services and timers
# - Container and images
# - Data directories
# - Database and user
# - Caddy configuration
# - System user and groups
#
# Usage: ansible-playbook playbooks/remove-nextcloud.yml -i inventory/hosts.yml
- name: Remove Nextcloud Installation
hosts: arch-vps
become: yes
gather_facts: yes
vars:
nextcloud_user: nextcloud
nextcloud_group: nextcloud
nextcloud_home: /opt/nextcloud
nextcloud_db_name: nextcloud
nextcloud_db_user: nextcloud
caddy_sites_enabled_dir: /etc/caddy/sites-enabled
tasks:
# ============================================
# Stop and Disable Services
# ============================================
- name: Stop and disable nextcloud-cron timer
systemd:
name: nextcloud-cron.timer
state: stopped
enabled: no
failed_when: false
- name: Stop and disable nextcloud-cron service
systemd:
name: nextcloud-cron.service
state: stopped
enabled: no
failed_when: false
- name: Stop and disable nextcloud service
systemd:
name: nextcloud.service
state: stopped
enabled: no
failed_when: false
# ============================================
# Remove Container and Images
# ============================================
- name: Remove nextcloud container (if running)
command: podman rm -f nextcloud
register: container_remove
changed_when: container_remove.rc == 0
failed_when: false
# ============================================
# Remove Systemd Units
# ============================================
- name: Remove nextcloud-cron systemd units
file:
path: "{{ item }}"
state: absent
loop:
- /etc/systemd/system/nextcloud-cron.timer
- /etc/systemd/system/nextcloud-cron.service
- name: Remove nextcloud quadlet file
file:
path: /etc/containers/systemd/nextcloud.container
state: absent
- name: Reload systemd daemon
systemd:
daemon_reload: yes
# ============================================
# Remove Database
# ============================================
- name: Drop nextcloud database
become_user: postgres
postgresql_db:
name: "{{ nextcloud_db_name }}"
state: absent
failed_when: false
- name: Drop nextcloud database user
become_user: postgres
postgresql_user:
name: "{{ nextcloud_db_user }}"
state: absent
failed_when: false
# ============================================
# Remove Caddy Configuration
# ============================================
- name: Remove nextcloud Caddy configuration
file:
path: "{{ caddy_sites_enabled_dir }}/nextcloud.caddy"
state: absent
notify: reload caddy
# ============================================
# Remove Data Directories
# ============================================
- name: Remove nextcloud home directory (including all data)
file:
path: "{{ nextcloud_home }}"
state: absent
# ============================================
# Remove User and Groups
# ============================================
- name: Remove nextcloud user
user:
name: "{{ nextcloud_user }}"
state: absent
remove: yes
force: yes
- name: Remove nextcloud group
group:
name: "{{ nextcloud_group }}"
state: absent
# ============================================
# Clean Up Remaining Files
# ============================================
- name: Find nextcloud-related files in /tmp
find:
paths: /tmp
patterns: "nextcloud*,nc_*"
file_type: any
register: tmp_files
- name: Remove nextcloud temp files
file:
path: "{{ item.path }}"
state: absent
loop: "{{ tmp_files.files }}"
when: tmp_files.files | length > 0
failed_when: false
- name: Remove caddy logs for nextcloud
file:
path: /var/log/caddy/nextcloud.log
state: absent
failed_when: false
# ============================================
# Verification
# ============================================
- name: Verify nextcloud service is removed
command: systemctl list-units --all nextcloud*
register: units_check
changed_when: false
- name: Verify nextcloud container is removed
command: podman ps -a --filter name=nextcloud
register: container_check
changed_when: false
- name: Display removal status
debug:
msg: |
✅ Nextcloud removal complete!
Removed components:
- ⏹️ Nextcloud service and cron timer
- 🐳 Container: {{ 'Removed' if container_remove.rc == 0 else 'Not found' }}
- 🗄️ Database: {{ nextcloud_db_name }}
- 📁 Data directory: {{ nextcloud_home }}
- 👤 System user: {{ nextcloud_user }}
- 🌐 Caddy configuration
Remaining services:
{{ units_check.stdout }}
Containers:
{{ container_check.stdout }}
handlers:
- name: reload caddy
systemd:
name: caddy
state: reloaded
failed_when: false

View File

@@ -43,15 +43,9 @@
- "Running kernel: {{ current_kernel.stdout }}" - "Running kernel: {{ current_kernel.stdout }}"
- "Latest modules: {{ latest_modules.stdout }}" - "Latest modules: {{ latest_modules.stdout }}"
- name: Test if nftables modules are available
command: nft list ruleset
register: nft_test_prereq
failed_when: false
changed_when: false
- name: Determine if reboot is needed - name: Determine if reboot is needed
set_fact: set_fact:
reboot_needed: "{{ current_kernel.stdout != latest_modules.stdout or nft_test_prereq.rc != 0 }}" reboot_needed: "{{ current_kernel.stdout != latest_modules.stdout }}"
- name: Reboot system if kernel/module mismatch detected - name: Reboot system if kernel/module mismatch detected
reboot: reboot:
@@ -65,16 +59,6 @@
timeout: 300 timeout: 300
when: reboot_needed | bool when: reboot_needed | bool
- name: Verify nftables is now available after reboot
command: nft list ruleset
register: nft_post_reboot
failed_when: false
changed_when: false
- name: Display post-reboot nftables status
debug:
msg: "nftables availability after reboot: {{ 'Working' if nft_post_reboot.rc == 0 else 'Failed' }}"
# ============================================ # ============================================
# SSH Hardening # SSH Hardening
# ============================================ # ============================================
@@ -139,23 +123,37 @@
name: nftables name: nftables
state: present state: present
- name: Create nftables configuration - name: Create nftables rules directory
file:
path: /etc/nftables.d
state: directory
mode: '0755'
- name: Create base nftables configuration
copy: copy:
content: | content: |
#!/usr/sbin/nft -f #!/usr/sbin/nft -f
# Main firewall table # Flush existing rules for clean slate
flush ruleset
# Main firewall table - Rick-Infra Security
# Architecture: Base rules -> Service rules -> Drop rule
table inet filter { table inet filter {
chain input { chain input {
type filter hook input priority 0; policy drop; type filter hook input priority 0; policy drop;
# ======================================
# Base Infrastructure Rules
# ======================================
# Allow loopback interface # Allow loopback interface
iif "lo" accept iif "lo" accept
# Allow established and related connections # Allow established and related connections
ct state established,related accept ct state established,related accept
# Allow SSH (port 22) # Allow SSH (port 22) - Infrastructure access
tcp dport 22 ct state new accept tcp dport 22 ct state new accept
# Allow HTTP and HTTPS for Caddy reverse proxy # Allow HTTP and HTTPS for Caddy reverse proxy
@@ -164,9 +162,6 @@
# Allow ping with rate limiting # Allow ping with rate limiting
icmp type echo-request limit rate 1/second accept icmp type echo-request limit rate 1/second accept
icmpv6 type echo-request limit rate 1/second accept icmpv6 type echo-request limit rate 1/second accept
# Log and drop everything else
counter drop
} }
chain forward { chain forward {
@@ -182,8 +177,42 @@
backup: yes backup: yes
register: nft_config_changed register: nft_config_changed
- name: Create nftables drop rule (loaded last)
copy:
content: |
# Final drop rule - Rick-Infra Security
# This file is loaded LAST to drop all unmatched traffic
# Service-specific rules in /etc/nftables.d/ are loaded before this
add rule inet filter input counter drop comment "Drop all other traffic"
dest: /etc/nftables.d/99-drop.nft
mode: '0644'
backup: yes
register: nft_drop_changed
- name: Create nftables loader script
copy:
content: |
#!/usr/sbin/nft -f
# Rick-Infra nftables loader
# Loads rules in correct order: base -> services -> drop
# Load base infrastructure rules
include "/etc/nftables.conf"
# Load service-specific rules (00-98 range)
include "/etc/nftables.d/[0-8]*.nft"
# Load final drop rule (99-drop.nft)
include "/etc/nftables.d/99-drop.nft"
dest: /etc/nftables-load.conf
mode: '0755'
backup: yes
register: nft_loader_changed
- name: Test nftables configuration syntax - name: Test nftables configuration syntax
command: nft -c -f /etc/nftables.conf command: nft -c -f /etc/nftables-load.conf
changed_when: false changed_when: false
failed_when: false failed_when: false
register: nft_test register: nft_test
@@ -193,18 +222,31 @@
msg: "nftables configuration test failed: {{ nft_test.stderr }}" msg: "nftables configuration test failed: {{ nft_test.stderr }}"
when: nft_test.rc != 0 when: nft_test.rc != 0
- name: Update nftables systemd service to use loader
lineinfile:
path: /usr/lib/systemd/system/nftables.service
regexp: '^ExecStart='
line: 'ExecStart=/usr/sbin/nft -f /etc/nftables-load.conf'
backup: yes
register: nft_service_changed
- name: Reload systemd daemon if service changed
systemd:
daemon_reload: yes
when: nft_service_changed.changed
- name: Flush existing nftables rules before applying new configuration - name: Flush existing nftables rules before applying new configuration
command: nft flush ruleset command: nft flush ruleset
failed_when: false failed_when: false
changed_when: false changed_when: false
when: nft_config_changed.changed when: nft_config_changed.changed or nft_drop_changed.changed or nft_loader_changed.changed
- name: Enable and start nftables service - name: Enable and start nftables service
systemd: systemd:
name: nftables name: nftables
enabled: yes enabled: yes
state: restarted state: restarted
when: nft_config_changed.changed when: nft_config_changed.changed or nft_drop_changed.changed or nft_loader_changed.changed or nft_service_changed.changed
- name: Wait for nftables to be active - name: Wait for nftables to be active
pause: pause:
@@ -280,8 +322,8 @@
sysctl_file: /etc/sysctl.d/99-security.conf sysctl_file: /etc/sysctl.d/99-security.conf
reload: yes reload: yes
loop: loop:
# Disable IP forwarding # Enable IP forwarding (required for container networking)
- { name: 'net.ipv4.ip_forward', value: '0' } - { name: 'net.ipv4.ip_forward', value: '1' }
- { name: 'net.ipv6.conf.all.forwarding', value: '0' } - { name: 'net.ipv6.conf.all.forwarding', value: '0' }
# Disable source routing # Disable source routing

View File

@@ -0,0 +1,57 @@
# jnss-web Static Site Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# WWW Redirect - apex is primary
www.{{ jnss_web_domain }} {
redir https://{{ jnss_web_domain }}{uri} permanent
}
# Primary Domain
{{ jnss_web_domain }} {
root * {{ jnss_web_root }}
file_server
# SPA routing - serve index.html for all routes
try_files {path} /index.html
# Security headers
header {
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "geolocation=(), microphone=(), camera=()"
}
# Cache static assets aggressively
@static {
path /_app/* /assets/* /icons/* *.ico *.png *.jpg *.jpeg *.svg *.webp *.woff *.woff2 *.css *.js
}
header @static {
Cache-Control "public, max-age=31536000, immutable"
Vary "Accept-Encoding"
}
# Cache HTML with shorter duration
@html {
path *.html /
}
header @html {
Cache-Control "public, max-age=3600, must-revalidate"
}
# Enable compression
encode gzip
# Logging
log {
output file /var/log/caddy/jnss-web.log {
roll_size 100mb
roll_keep 5
}
format json {
time_format "2006-01-02T15:04:05.000Z07:00"
}
level INFO
}
}

33
production.yml Normal file
View File

@@ -0,0 +1,33 @@
---
# Production Services Deployment
#
# Deploys production services requiring high uptime to mini-vps including:
# - Caddy web server
# - Sigvild Gallery (wedding photo gallery)
# - Devigo (sales training company website)
#
# Usage:
# ansible-playbook playbooks/production.yml
# ansible-playbook playbooks/production.yml --tags devigo
# - import_playbook: security.yml
- name: Deploy Production Services
hosts: production
become: true
gather_facts: true
pre_tasks:
# Workaround: Manually load group_vars due to Ansible 2.20 variable loading issue
- name: Load production group variables
include_vars:
dir: "{{ playbook_dir }}/group_vars/production"
extensions: ['yml']
tags: always
roles:
- role: devigo
tags: ['devigo', 'website', 'sales', 'oauth']
- role: sigvild-gallery
tags: ['sigvild', 'gallery', 'wedding']

55
rick-infra.yml Normal file
View File

@@ -0,0 +1,55 @@
---
# Homelab Infrastructure Deployment
#
# Deploys personal homelab services to arch-vps including:
# - PostgreSQL database
# - Valkey cache/session store
# - Podman container runtime
# - Caddy web server
# - Nextcloud cloud storage
# - Authentik SSO/authentication
# - Gitea git hosting
# - Vaultwarden password manager
# - Metrics (VictoriaMetrics, Grafana, node_exporter)
#
# Usage:
# ansible-playbook rick-infra.yml
# ansible-playbook rick-infra.yml --tags metrics
# - import_playbook: playbooks/security.yml
- name: Deploy Homelab Infrastructure
hosts: homelab
become: true
gather_facts: true
tasks:
# - name: Deploy Caddy
# include_role:
# name: caddy
# tags: ['caddy']
- name: Deploy Metrics Stack
include_role:
name: metrics
tags: ['metrics', 'monitoring', 'grafana', 'victoriametrics']
# - name: Deploy Authentik
# include_role:
# name: authentik
# tags: ['authentik', 'sso', 'auth']
# - name: Deploy Gitea
# include_role:
# name: gitea
# tags: ['gitea', 'git', 'development']
# - name: Deploy Nextcloud
# include_role:
# name: nextcloud
# tags: ['nextcloud', 'cloud', 'storage']
# - name: Deploy Vaultwarden
# include_role:
# name: vaultwarden
# tags: ['vaultwarden', 'vault', 'password-manager', 'security']

View File

@@ -16,33 +16,13 @@
state: restarted state: restarted
daemon_reload: true daemon_reload: true
- name: restart authentik server - name: stop authentik pod
systemd: systemd:
name: "{{ authentik_container_server_name }}" name: "authentik-pod"
state: restarted
daemon_reload: true
- name: restart authentik worker
systemd:
name: "{{ authentik_container_worker_name }}"
state: restarted
daemon_reload: true
- name: stop authentik services
systemd:
name: "{{ item }}"
state: stopped state: stopped
loop:
- "{{ authentik_container_worker_name }}"
- "{{ authentik_container_server_name }}"
- "authentik-pod"
- name: start authentik services - name: start authentik pod
systemd: systemd:
name: "{{ item }}" name: "authentik-pod"
state: started state: started
daemon_reload: true daemon_reload: true
loop:
- "authentik-pod"
- "{{ authentik_container_server_name }}"
- "{{ authentik_container_worker_name }}"

View File

@@ -52,8 +52,6 @@
backup: true backup: true
notify: notify:
- restart authentik pod - restart authentik pod
- restart authentik server
- restart authentik worker
tags: [config] tags: [config]
- name: Create Quadlet systemd directory (system scope) - name: Create Quadlet systemd directory (system scope)
@@ -74,8 +72,6 @@
notify: notify:
- reload systemd - reload systemd
- restart authentik pod - restart authentik pod
- restart authentik server
- restart authentik worker
tags: [containers, deployment] tags: [containers, deployment]
- name: Deploy Caddy configuration - name: Deploy Caddy configuration
@@ -85,7 +81,6 @@
owner: root owner: root
group: "{{ caddy_user }}" group: "{{ caddy_user }}"
mode: '0644' mode: '0644'
backup: true
notify: reload caddy notify: reload caddy
tags: [caddy, reverse-proxy] tags: [caddy, reverse-proxy]

View File

@@ -1,7 +1,7 @@
--- ---
- name: Check if DNS challenge is needed - name: Check if DNS challenge is needed
set_fact: set_fact:
dns_challenge_needed: "{{ caddy_dns_provider == 'cloudflare' and cloudflare_api_token != '' }}" dns_challenge_needed: "{{ caddy_dns_provider == 'cloudflare' }}"
- name: Check if Caddy is already installed - name: Check if Caddy is already installed
command: /usr/bin/caddy version command: /usr/bin/caddy version
@@ -120,7 +120,7 @@
owner: root owner: root
group: "{{ caddy_user }}" group: "{{ caddy_user }}"
mode: '0640' mode: '0640'
backup: yes backup: no
notify: reload caddy notify: reload caddy
- name: Check Caddyfile syntax (basic check) - name: Check Caddyfile syntax (basic check)

View File

@@ -21,43 +21,3 @@
# Import service configurations # Import service configurations
import {{ caddy_sites_enabled_dir }}/* import {{ caddy_sites_enabled_dir }}/*
# Primary domain: {{ caddy_domain }}
{{ caddy_domain }} {
{% if caddy_tls_enabled %}
{% if caddy_dns_provider == "cloudflare" and cloudflare_api_token %}
# DNS challenge for automatic TLS
tls {
dns cloudflare {{ cloudflare_api_token }}
resolvers {{ caddy_dns_resolvers | join(' ') }}
}
{% elif caddy_tls_email %}
# HTTP challenge for automatic TLS
tls {{ caddy_tls_email }}
{% endif %}
{% endif %}
# Serve static content
root * {{ caddy_default_site_root }}
file_server
# Logging
log {
{% if caddy_log_format == "json" %}
output file {{ caddy_log_dir }}/{{ caddy_domain | replace('.', '_') }}.log {
roll_size 100mb
roll_keep 5
}
format json {
time_format "2006-01-02T15:04:05.000Z07:00"
}
level {{ caddy_log_level }}
{% else %}
output file {{ caddy_log_dir }}/{{ caddy_domain | replace('.', '_') }}.log {
roll_size 100mb
roll_keep 5
}
level {{ caddy_log_level }}
{% endif %}
}
}

612
roles/devigo/README.md Normal file
View File

@@ -0,0 +1,612 @@
# Devigo Ansible Role
Deploys the Devigo sales training website (devigo.no) to production infrastructure using Podman Quadlet and Caddy reverse proxy.
## Description
Devigo is a Hugo-based static marketing website deployed via a hybrid approach:
- **Infrastructure**: Managed by Ansible (this role)
- **Application**: Built and deployed via GitHub Actions CI/CD
- **Runtime**: Podman Quadlet containers with systemd integration
- **Web Server**: Caddy reverse proxy with automatic HTTPS
This role deploys two services:
1. **devigo-site**: The main website container (Hugo + Nginx)
2. **devigo-decap-oauth**: OAuth2 proxy for Decap CMS content management
## Architecture
```
GitHub Actions (CI/CD)
Build Hugo site → Docker image → GHCR
SSH to mini-vps → systemctl restart devigo-site
Podman Quadlet pulls latest image
Systemd manages container lifecycle
Caddy reverse proxy (HTTPS)
Internet (devigo.no)
```
## Requirements
### Control Machine
- Ansible 2.20+
- Vault password for encrypted credentials
- SSH access to production host
### Target Host
- Arch Linux (or compatible)
- Podman 5.7+
- Caddy with Cloudflare DNS plugin
- Systemd with Quadlet support
## Role Variables
### Required Variables (set in group_vars/production/main.yml)
```yaml
# Domains
devigo_domain: "devigo.no" # Apex domain (primary)
devigo_www_domain: "www.devigo.no" # WWW subdomain (redirects to apex)
devigo_primary_domain: "devigo.no" # Which domain is primary
# OAuth Service
devigo_oauth_domain: "decap.jnss.me" # OAuth proxy domain
devigo_oauth_client_id: "{{ vault_devigo_oauth_client_id }}"
devigo_oauth_client_secret: "{{ vault_devigo_oauth_client_secret }}"
# IMPORTANT: Must be comma-separated string, NOT YAML array
# The decapcms-oauth2 service requires TRUSTED_ORIGINS (plural) as a simple string
devigo_oauth_trusted_origins: "https://devigo.no,https://www.devigo.no"
# Container Registry
devigo_ghcr_image: "ghcr.io/jnschaffer/rustan:prod"
```
### Optional Variables (defaults/main.yml)
```yaml
devigo_container_name: "devigo-site" # Container name
devigo_container_port: 80 # Nginx port inside container
devigo_oauth_user: "devigo-oauth" # OAuth service user
devigo_oauth_home: "/opt/devigo-oauth" # OAuth config directory
devigo_oauth_container_name: "devigo-decap-oauth"
devigo_oauth_container_port: 12000 # OAuth service port
```
### Vault Variables (group_vars/production/vault.yml - encrypted)
```yaml
vault_devigo_oauth_client_id: "Ov23liNvuDONNB0dhwj6"
vault_devigo_oauth_client_secret: "5dc4d17cfa7d550dd1b5ade0e33d1c9689bbbfad"
```
## Dependencies
This role depends on:
- `podman` - Container runtime
- `caddy` - Reverse proxy with TLS
These are automatically included via `meta/main.yml`.
## Deployed Services
### 1. devigo-site (Main Website)
**Systemd Unit**: `devigo-site.service`
**Quadlet File**: `/etc/containers/systemd/devigo-site.container`
**Container**: `ghcr.io/jnschaffer/rustan:prod`
**Network**: `caddy` (Podman network)
**Health Check**: Verifies `/usr/share/nginx/html/index.html` exists
**Features**:
- Auto-update support via `podman auto-update`
- Systemd managed (auto-restart on failure)
- Health monitoring
- Connected to Caddy network for reverse proxy
### 2. devigo-decap-oauth (CMS OAuth Proxy)
**Systemd Unit**: `devigo-decap-oauth.service`
**Quadlet File**: `/etc/containers/systemd/devigo-decap-oauth.container`
**Container**: `docker.io/alukovenko/decapcms-oauth2:latest`
**Port**: `127.0.0.1:12000:12000`
**Purpose**: OAuth2 proxy for Decap CMS GitHub authentication
**Features**:
- Enables content editing via Decap CMS at `/admin`
- Authenticates with GitHub OAuth app
- Proxied via Caddy at `decap.jnss.me`
## Caddy Configuration
### Site Configuration
- **devigo.no** - Primary domain, serves content
- **www.devigo.no** - Redirects to apex domain
- Reverse proxy to `devigo-site` container via Podman network
- Security headers (X-Frame-Options, CSP, etc.)
- JSON logging to `/var/log/caddy/devigo.log`
### OAuth Configuration
- **decap.jnss.me** - OAuth proxy endpoint
- Reverse proxy to `localhost:12000`
- Required for Decap CMS `/admin` functionality
## GitHub Actions Integration
### Workflow Overview
The site is deployed via GitHub Actions when code is pushed to the `prod` branch:
1. **Build Phase**: Hugo site built in Docker container
2. **Push Phase**: Image pushed to `ghcr.io/jnschaffer/rustan:prod`
3. **Deploy Phase**: SSH to mini-vps, restart systemd service
### Deploy Script (in .github/workflows/deploy.yml)
```yaml
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/prod'
steps:
- name: Deploy to mini-vps
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
# Check for updates
podman auto-update --dry-run devigo-site
# Restart service to pull and use new image
systemctl restart devigo-site
# Verify deployment
sleep 5
systemctl is-active devigo-site
podman ps | grep devigo-site
# Cleanup
podman system prune -f
```
### Required GitHub Secrets
Configure in repository settings (`https://github.com/jnschaffer/rustan/settings/secrets/actions`):
- `VPS_HOST`: `72.62.91.251` (mini-vps IP)
- `VPS_USER`: `root`
- `VPS_SSH_KEY`: SSH private key for mini-vps access
## Usage
### Deploy Infrastructure
```bash
# Deploy all devigo infrastructure
ansible-playbook playbooks/production.yml --tags devigo
# Deploy all production services
ansible-playbook playbooks/production.yml
```
### Manual Operations
```bash
# Check service status
ansible mini-vps -m shell -a "systemctl status devigo-site"
ansible mini-vps -m shell -a "systemctl status devigo-decap-oauth"
# View logs
ansible mini-vps -m shell -a "journalctl -u devigo-site -n 50"
ansible mini-vps -m shell -a "journalctl -u devigo-decap-oauth -n 50"
# Check containers
ansible mini-vps -m shell -a "podman ps"
# Manually restart services
ansible mini-vps -m shell -a "systemctl restart devigo-site"
ansible mini-vps -m shell -a "systemctl restart devigo-decap-oauth"
# Check for available updates
ansible mini-vps -m shell -a "podman auto-update --dry-run"
# Force pull latest image
ansible mini-vps -m shell -a "systemctl restart devigo-site"
```
### Testing Deployment
```bash
# Test site (after DNS update)
curl -I https://devigo.no
curl -I https://www.devigo.no # Should redirect to apex
# Test OAuth proxy
curl -I https://decap.jnss.me
# Test from control machine with IP
curl -I -k -H "Host: devigo.no" https://72.62.91.251
```
## Content Management
### Decap CMS Access
1. Navigate to `https://devigo.no/admin`
2. Click "Login with GitHub"
3. Authenticate via OAuth proxy (`decap.jnss.me`)
4. Edit content via CMS interface
5. Changes commit to GitHub → triggers rebuild → auto-deploy
### Direct Git Workflow
```bash
cd ~/rustan
# Make changes to content/norwegian/
git add .
git commit -m "Update content"
git push origin prod
# GitHub Actions builds and deploys automatically
```
## DNS Configuration
Required DNS records:
```
devigo.no A 72.62.91.251
www.devigo.no CNAME devigo.no.
decap.jnss.me A 72.62.91.251
```
TLS certificates are automatically obtained via Caddy + Cloudflare DNS challenge.
## Troubleshooting
### Container Won't Start
```bash
# Check Quadlet file syntax
ansible mini-vps -m shell -a "cat /etc/containers/systemd/devigo-site.container"
# Check systemd status
ansible mini-vps -m shell -a "systemctl status devigo-site"
# View detailed logs
ansible mini-vps -m shell -a "journalctl -u devigo-site -n 100"
# Check if image exists
ansible mini-vps -m shell -a "podman images | grep rustan"
```
### OAuth Not Working
```bash
# Verify OAuth container running
ansible mini-vps -m shell -a "systemctl status devigo-decap-oauth"
# Check OAuth logs
ansible mini-vps -m shell -a "journalctl -u devigo-decap-oauth -n 50"
# Test OAuth endpoint locally
ansible mini-vps -m shell -a "curl -I http://localhost:12000"
# Verify environment variables
ansible mini-vps -m shell -a "cat /opt/devigo-oauth/decap-oauth.env"
```
### TLS Certificate Issues
```bash
# Check Caddy logs
ansible mini-vps -m shell -a "journalctl -u caddy -n 100"
# Verify certificate obtained
ansible mini-vps -m shell -a "ls -la /var/lib/caddy/.local/share/caddy/certificates"
# Force certificate renewal
ansible mini-vps -m shell -a "systemctl reload caddy"
```
### GitHub Actions Deployment Fails
1. Verify secrets are set in GitHub repository
2. Check SSH connectivity: `ssh -i /path/to/key root@72.62.91.251`
3. Verify image was pushed to GHCR: `https://github.com/jnschaffer/rustan/pkgs/container/rustan`
4. Check GitHub Actions logs for detailed error messages
## Automatic Updates (Future Enhancement)
For fully automatic updates without GitHub Actions triggering:
### Systemd Timer Approach
Create a systemd timer to run `podman auto-update` daily:
```ini
# /etc/systemd/system/podman-auto-update.timer
[Unit]
Description=Podman Auto Update Timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
```
```ini
# /etc/systemd/system/podman-auto-update.service
[Unit]
Description=Podman Auto Update Service
[Service]
Type=oneshot
ExecStart=/usr/bin/podman auto-update
ExecStartPost=/usr/bin/systemctl restart devigo-site
```
Enable timer:
```bash
systemctl enable --now podman-auto-update.timer
```
This provides automatic daily checks for new images independent of GitHub Actions.
## Files Deployed
```
/etc/containers/systemd/
├── devigo-site.container # Website Quadlet
└── devigo-decap-oauth.container # OAuth Quadlet
/etc/caddy/sites-enabled/
├── devigo.caddy # Site reverse proxy config
└── devigo-decap-oauth.caddy # OAuth reverse proxy config
/opt/devigo-oauth/
└── decap-oauth.env # OAuth environment variables
/var/log/caddy/
├── devigo.log # Site access logs
└── devigo-decap-oauth.log # OAuth access logs
```
## Security Considerations
1. **OAuth Credentials**: Stored encrypted in Ansible Vault
2. **Container Security**: Runs with `NoNewPrivileges=true`
3. **Network Isolation**: Containers only on `caddy` network
4. **TLS**: All traffic encrypted via Cloudflare DNS challenge
5. **SSH Keys**: GitHub Actions uses dedicated SSH key
6. **Image Verification**: Images pulled from trusted GHCR
## Notes
- Hugo version on control machine (v0.152.2) is newer than package.json (v0.144.2) - builds work fine
- Contact form uses Airform.io service (patrick@devigo.no endpoint)
- Build time: ~3-5 seconds locally, ~30-60 seconds in GitHub Actions
- Image size: ~50MB (Hugo + Nginx Alpine-based)
- The role does not build the Hugo site - that's handled by GitHub Actions
- DNS must point to mini-vps (72.62.91.251) before site will be accessible
## Example Playbook
```yaml
---
- name: Deploy Production Services
hosts: production
become: true
roles:
- role: devigo
tags: ['devigo', 'website', 'sales', 'oauth']
```
## License
MIT
## Author
Created for rick-infra project by Claude Code
## Troubleshooting
### OAuth "Origin not trusted" Error
**Symptom**: Browser console shows `Origin not trusted: https://devigo.no` when clicking "Login with GitHub"
**Cause**: The `TRUSTED_ORIGINS` environment variable is incorrectly formatted or using the wrong variable name.
**Solution**:
1. Check the deployed env file:
```bash
ansible mini-vps -m shell -a "cat /opt/devigo-oauth/decap-oauth.env"
```
2. Verify it contains:
```
TRUSTED_ORIGINS=https://devigo.no,https://www.devigo.no
```
**NOT**:
- `TRUSTED_ORIGIN=` (singular - only supports one origin)
- `TRUSTED_ORIGINS=['https://...']` (YAML array - incorrect format)
3. If incorrect, ensure `roles/devigo/defaults/main.yml` contains:
```yaml
devigo_oauth_trusted_origins: "https://devigo.no,https://www.devigo.no"
```
4. Redeploy and restart:
```bash
ansible-playbook playbooks/production.yml --tags devigo
ansible mini-vps -m shell -a "systemctl restart devigo-decap-oauth"
```
### Decap CMS "Response was not yaml" Error
**Symptom**: Decap CMS shows error: `Response for config.yml was not yaml. (Content-Type: application/octet-stream)`
**Cause**: The `/admin/config.yml` file is not being served with `Content-Type: text/yaml`
**Solution**:
1. Verify nginx configuration in the container has the location block:
```bash
ansible mini-vps -m shell -a "podman exec devigo-site grep -A 3 'location.*yml' /etc/nginx/nginx.conf"
```
2. Should contain:
```nginx
location ~ \.(yml|yaml)$ {
default_type text/yaml;
}
```
3. Test the MIME type:
```bash
curl -I https://devigo.no/admin/config.yml | grep content-type
# Expected: content-type: text/yaml
```
4. If missing, check the `rustan` repository's `nginx.conf` file and redeploy.
### HTML/CSS Files Show as "application/octet-stream"
**Symptom**: Site doesn't render correctly, browser shows files as `application/octet-stream`
**Cause**: Server-level `types {}` block in nginx.conf overriding all MIME types
**Solution**:
1. Check nginx.conf in the container:
```bash
ansible mini-vps -m shell -a "podman exec devigo-site cat /etc/nginx/nginx.conf | head -20"
```
2. Ensure there is **NO** `types { ... }` block at the server level
3. YAML MIME type should be handled by location block only (see above)
4. Fix in `rustan` repository, commit, and push to trigger rebuild
### Cannot Pull Private GHCR Images
**Symptom**: `podman pull` fails with 401 Unauthorized or authentication errors
**Cause**: Missing or incorrect GitHub Container Registry authentication
**Solution**:
1. Verify auth.json exists:
```bash
ansible mini-vps -m shell -a "ls -la /etc/containers/auth.json"
# Expected: -rw------- 1 root root
```
2. Test authentication:
```bash
ansible mini-vps -m shell -a "podman login ghcr.io --get-login"
# Expected: jnschaffer (your GitHub username)
```
3. If fails, verify the Quadlet has the environment variable:
```bash
ansible mini-vps -m shell -a "systemctl cat devigo-site.service | grep REGISTRY_AUTH_FILE"
# Expected: Environment=REGISTRY_AUTH_FILE=/etc/containers/auth.json
```
4. If missing, redeploy the role:
```bash
ansible-playbook playbooks/production.yml --tags devigo,podman
```
### Service Won't Start After GitHub Actions Deployment
**Symptom**: `systemctl status devigo-site` shows failed or inactive
**Causes and Solutions**:
1. **Image pull failed**:
```bash
ansible mini-vps -m shell -a "journalctl -u devigo-site -n 50"
# Look for "401 Unauthorized" - see authentication troubleshooting above
```
2. **Port already in use**:
```bash
ansible mini-vps -m shell -a "ss -tlnp | grep 9080"
# If occupied, stop conflicting service or change port
```
3. **Corrupted image**:
```bash
ansible mini-vps -m shell -a "podman pull --force ghcr.io/jnschaffer/rustan:prod"
ansible mini-vps -m shell -a "systemctl restart devigo-site"
```
## Architecture Decisions
### Why Apex Domain (devigo.no) Over WWW?
**Decision**: Use `devigo.no` as primary, redirect `www.devigo.no` → `devigo.no`
**Rationale**:
- Shorter, cleaner URLs
- Better for SEO (single canonical domain)
- Simpler TLS certificate management
- Modern web convention (www is legacy)
### Why JavaScript Initialization for Decap CMS?
**Decision**: Use `CMS.init()` with manual config loading instead of `<link>` tag
**Rationale**:
- Eliminates dependency on YAML MIME type for `<link>` element
- More portable across different server configurations
- Explicit and debuggable (clear in source what's happening)
- Matches working configuration from previous deployment
**Trade-off**: Slightly more verbose HTML (3 extra lines of JavaScript)
### Why Location Block for YAML MIME Type?
**Decision**: Use `location ~ \.(yml|yaml)$ { default_type text/yaml; }` instead of server-level `types {}` block
**Rationale**:
- Surgical approach - only affects YAML files
- Doesn't override nginx's default MIME types for HTML/CSS/JS
- Easier to understand and maintain
- Prevents accidental breaking of other file types
**Trade-off**: Requires location block in nginx.conf (but this is minimal)
### Why Quadlet Over Docker Compose?
**Decision**: Use Podman Quadlet with systemd integration instead of docker-compose
**Rationale**:
- Native systemd integration (journald logs, dependency management)
- Better security (rootless capable, no daemon required)
- Automatic service management via systemd generators
- Auto-update support via `podman auto-update`
- Aligns with Ansible infrastructure-as-code approach
**Trade-off**: Less familiar to developers used to Docker Compose (but well-documented)
## Related Documentation
- [DEVIGO Deployment Guide](../../docs/devigo-deployment-guide.md) - Complete deployment walkthrough
- [Podman Role](../podman/README.md) - Container runtime configuration
- [Caddy Role](../caddy/README.md) - Reverse proxy and TLS
- [Service Integration Guide](../../docs/service-integration-guide.md) - How services interact
## License
Part of the rick-infra project. See repository root for license information.

View File

@@ -0,0 +1,30 @@
---
# Devigo Infrastructure - Default Variables
# Domains
devigo_domain: "devigo.no"
devigo_www_domain: "www.devigo.no"
devigo_primary_domain: "devigo.no" # Apex is primary
# Container configuration
devigo_container_name: "devigo-site"
devigo_host_port: 9080 # Port published to localhost
devigo_container_port: 80 # Nginx inside container
# GitHub Container Registry
devigo_ghcr_image: "ghcr.io/jnschaffer/rustan:prod"
# Decap OAuth configuration
devigo_oauth_domain: "decap.jnss.me"
devigo_oauth_user: "devigo-oauth"
devigo_oauth_home: "/opt/devigo-oauth"
devigo_oauth_container_name: "devigo-decap-oauth"
devigo_oauth_container_image: "docker.io/alukovenko/decapcms-oauth2:latest"
devigo_oauth_container_port: 12000
devigo_oauth_client_id: "{{ vault_devigo_oauth_client_id }}"
devigo_oauth_client_secret: "{{ vault_devigo_oauth_client_secret }}"
devigo_oauth_trusted_origins: "https://devigo.no,https://www.devigo.no"
# Caddy integration (assumes caddy role provides these)
# caddy_sites_enabled_dir: /etc/caddy/sites-enabled
# caddy_user: caddy

View File

@@ -0,0 +1,19 @@
---
- name: reload systemd
systemd:
daemon_reload: yes
- name: restart devigo-decap-oauth
systemd:
name: "{{ devigo_oauth_container_name }}"
state: restarted
- name: restart devigo-site
systemd:
name: devigo-site
state: restarted
- name: reload caddy
systemd:
name: caddy
state: reloaded

View File

@@ -0,0 +1,4 @@
---
dependencies:
- role: podman
- role: caddy

View File

@@ -0,0 +1,93 @@
---
# Devigo Deployment - Main Tasks
# OAuth Service Setup
- name: Create decap-oauth user
user:
name: "{{ devigo_oauth_user }}"
system: yes
shell: /usr/sbin/nologin
home: "{{ devigo_oauth_home }}"
create_home: yes
- name: Create decap-oauth directories
file:
path: "{{ devigo_oauth_home }}"
state: directory
owner: "{{ devigo_oauth_user }}"
group: "{{ devigo_oauth_user }}"
mode: '0755'
- name: Deploy OAuth environment file
template:
src: devigo-decap-oauth.env.j2
dest: "{{ devigo_oauth_home }}/decap-oauth.env"
owner: "{{ devigo_oauth_user }}"
group: "{{ devigo_oauth_user }}"
mode: '0600'
notify: restart devigo-decap-oauth
- name: Create Quadlet systemd directory
file:
path: /etc/containers/systemd
state: directory
owner: root
group: root
mode: '0755'
- name: Deploy Quadlet container file
template:
src: devigo-decap-oauth.container
dest: "/etc/containers/systemd/{{ devigo_oauth_container_name }}.container"
owner: root
group: root
mode: '0644'
register: quadlet_deployed
- name: Reload systemd to discover Quadlet container
systemd:
daemon_reload: yes
- name: Deploy OAuth Caddy configuration
template:
src: devigo-decap-oauth.caddy.j2
dest: "{{ caddy_sites_enabled_dir }}/devigo-decap-oauth.caddy"
owner: root
group: "{{ caddy_user }}"
mode: '0644'
notify: reload caddy
- name: Enable and start decap-oauth service
systemd:
name: "{{ devigo_oauth_container_name }}"
enabled: yes
state: started
# Devigo Site Quadlet Setup
- name: Deploy devigo-site Quadlet container file
template:
src: devigo-site.container
dest: "/etc/containers/systemd/devigo-site.container"
owner: root
group: root
mode: '0644'
register: devigo_site_quadlet
- name: Reload systemd to discover devigo-site Quadlet
systemd:
daemon_reload: yes
- name: Deploy Caddy configuration for devigo site
template:
src: devigo.caddy.j2
dest: "{{ caddy_sites_enabled_dir }}/devigo.caddy"
owner: root
group: "{{ caddy_user }}"
mode: '0644'
notify: reload caddy
- name: Enable and start devigo-site service
systemd:
name: devigo-site
enabled: yes
state: started

View File

@@ -0,0 +1,32 @@
---
# Set up Docker infrastructure for Devigo deployment
- name: Create devigo deployment directory
file:
path: "{{ devigo_docker_dir }}"
state: directory
owner: root
group: root
mode: '0755'
- name: Create caddy Docker network
containers.podman.podman_network:
name: caddy
state: present
- name: Deploy docker-compose.yml
template:
src: docker-compose.yml.j2
dest: "{{ devigo_compose_file }}"
owner: root
group: root
mode: '0644'
- name: Deploy Caddy configuration for devigo site
template:
src: devigo.caddy.j2
dest: "{{ caddy_sites_enabled_dir }}/devigo.caddy"
owner: root
group: "{{ caddy_user }}"
mode: '0644'
notify: reload caddy

View File

@@ -0,0 +1,54 @@
---
# Set up Decap OAuth service
- name: Create decap-oauth user
user:
name: "{{ devigo_oauth_user }}"
system: yes
shell: /usr/sbin/nologin
home: "{{ devigo_oauth_home }}"
create_home: yes
- name: Create decap-oauth directories
file:
path: "{{ devigo_oauth_home }}"
state: directory
owner: "{{ devigo_oauth_user }}"
group: "{{ devigo_oauth_user }}"
mode: '0755'
- name: Deploy OAuth environment file
template:
src: devigo-decap-oauth.env.j2
dest: "{{ devigo_oauth_home }}/decap-oauth.env"
owner: "{{ devigo_oauth_user }}"
group: "{{ devigo_oauth_user }}"
mode: '0600'
notify: restart devigo-decap-oauth
- name: Deploy Quadlet container file
template:
src: devigo-decap-oauth.container
dest: "/etc/containers/systemd/{{ devigo_oauth_container_name }}.container"
owner: root
group: root
mode: '0644'
notify:
- reload systemd
- restart devigo-decap-oauth
- name: Deploy OAuth Caddy configuration
template:
src: devigo-decap-oauth.caddy.j2
dest: "{{ caddy_sites_enabled_dir }}/devigo-decap-oauth.caddy"
owner: root
group: "{{ caddy_user }}"
mode: '0644'
notify: reload caddy
- name: Enable and start decap-oauth service
systemd:
name: "{{ devigo_oauth_container_name }}"
enabled: yes
state: started
daemon_reload: yes

View File

@@ -0,0 +1,25 @@
# Decap CMS OAuth2 Proxy
# Generated by Ansible - DO NOT EDIT MANUALLY
{{ devigo_oauth_domain }} {
reverse_proxy 127.0.0.1:{{ devigo_oauth_container_port }}
# Security headers
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
# Logging
log {
output file /var/log/caddy/devigo-decap-oauth.log {
roll_size 100mb
roll_keep 5
}
format json {
time_format "2006-01-02T15:04:05.000Z07:00"
}
level INFO
}
}

View File

@@ -0,0 +1,25 @@
[Unit]
Description=Decap CMS OAuth2 Proxy for Devigo
After=network-online.target
Wants=network-online.target
[Container]
Image={{ devigo_oauth_container_image }}
ContainerName={{ devigo_oauth_container_name }}
AutoUpdate=registry
# Environment file with secrets
EnvironmentFile={{ devigo_oauth_home }}/decap-oauth.env
# Network
PublishPort=127.0.0.1:{{ devigo_oauth_container_port }}:12000
# Security
NoNewPrivileges=true
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,10 @@
# Decap OAuth Environment Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
OAUTH_CLIENT_ID={{ devigo_oauth_client_id }}
OAUTH_CLIENT_SECRET={{ devigo_oauth_client_secret }}
SERVER_PORT=12000
# TRUSTED_ORIGINS must be comma-separated string (not YAML array)
# Example: https://example.com,https://www.example.com
TRUSTED_ORIGINS={{ devigo_oauth_trusted_origins }}

View File

@@ -0,0 +1,31 @@
[Unit]
Description=Devigo Website - Sales Training Company
After=network-online.target caddy.service
Wants=network-online.target
Requires=caddy.service
[Container]
Image=ghcr.io/jnschaffer/rustan:prod
ContainerName=devigo-site
AutoUpdate=registry
Pull=newer
# Port mapping - publish to localhost only
PublishPort=127.0.0.1:9080:80
# Security
NoNewPrivileges=true
# Health check - check if nginx is responding
HealthCmd=/usr/bin/curl -f http://localhost:80/ || exit 1
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
[Service]
Environment=REGISTRY_AUTH_FILE=/etc/containers/auth.json
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,33 @@
# Devigo Website - Reverse Proxy to Containerized Site
# Generated by Ansible - DO NOT EDIT MANUALLY
# Redirect www to apex (apex is primary per user preference)
{{ devigo_www_domain }} {
redir https://{{ devigo_domain }}{uri} permanent
}
# Primary domain (apex)
{{ devigo_domain }} {
reverse_proxy localhost:9080
# Security headers
header {
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "geolocation=(), microphone=(), camera=()"
}
# Logging
log {
output file /var/log/caddy/devigo.log {
roll_size 100mb
roll_keep 5
}
format json {
time_format "2006-01-02T15:04:05.000Z07:00"
}
level INFO
}
}

View File

@@ -8,16 +8,22 @@ Self-contained Gitea Git service for rick-infra following the established archit
-**Native Arch installation**: Uses pacman packages -**Native Arch installation**: Uses pacman packages
-**PostgreSQL integration**: Uses shared PostgreSQL infrastructure -**PostgreSQL integration**: Uses shared PostgreSQL infrastructure
-**Caddy integration**: Deploys reverse proxy configuration -**Caddy integration**: Deploys reverse proxy configuration
-**Dual SSH modes**: Passthrough (default) or dedicated SSH server
-**Flexible domains**: Separate HTTP and SSH domains
-**Security hardened**: SystemD restrictions and secure defaults -**Security hardened**: SystemD restrictions and secure defaults
-**Firewall management**: Automatic nftables configuration per mode
-**fail2ban protection**: Brute force protection for SSH authentication
-**Production ready**: HTTPS, SSH access, LFS support -**Production ready**: HTTPS, SSH access, LFS support
## Architecture ## Architecture
- **Dependencies**: PostgreSQL infrastructure role - **Dependencies**: PostgreSQL infrastructure role
- **Database**: Self-managed gitea database and user - **Database**: Self-managed gitea database and user
- **Network**: HTTP on :3000, SSH on :2222 (localhost) - **Network**: HTTP on :3000 (localhost), SSH via system SSH (port 22) or dedicated (port 2222)
- **Web access**: https://git.domain.com (via Caddy) - **Web access**: https://git.jnss.me (via Caddy reverse proxy)
- **SSH access**: ssh://git@git.domain.com:2222 - **SSH access**: git@jnss.me:user/repo.git (passthrough mode, default)
- **Firewall**: Managed per SSH mode (no extra ports in passthrough)
- **Security**: fail2ban protects SSH authentication (system or dedicated jail)
## Configuration ## Configuration
@@ -27,11 +33,13 @@ Key variables (defaults in `defaults/main.yml`):
# Service # Service
gitea_service_enabled: true gitea_service_enabled: true
gitea_http_port: 3000 gitea_http_port: 3000
gitea_ssh_port: 2222
# Domain # Domain Configuration
gitea_subdomain: "git" gitea_http_domain: "git.jnss.me" # Web interface
gitea_domain: "{{ caddy_domain }}" gitea_ssh_domain: "jnss.me" # SSH/Git operations
# SSH Mode
gitea_ssh_mode: "passthrough" # or "dedicated"
# Database (self-managed) # Database (self-managed)
gitea_db_name: "gitea" gitea_db_name: "gitea"
@@ -44,11 +52,186 @@ gitea_disable_registration: false
gitea_enable_lfs: true gitea_enable_lfs: true
``` ```
## Domain Configuration
Gitea uses **separate domains** for HTTP and SSH access, providing flexibility and cleaner URLs:
### HTTP Domain (`gitea_http_domain`)
- **Purpose**: Web interface access
- **Default**: `git.jnss.me`
- **Example**: `https://git.jnss.me`
- **Used for**: Browsing repos, managing settings, viewing commits
### SSH Domain (`gitea_ssh_domain`)
- **Purpose**: Git clone/push/pull operations
- **Default**: `jnss.me` (root domain)
- **Example**: `git@jnss.me:user/repo.git`
- **Used for**: Git operations over SSH
**Why Separate Domains?**
- ✅ Cleaner SSH URLs (no redundant "git" subdomain)
- ✅ Flexibility to use completely different domains
- ✅ Matches industry standards (GitHub, GitLab use root domain for SSH)
- ✅ Professional appearance
**Configuration Examples:**
```yaml
# Standard setup (recommended)
gitea_http_domain: "git.jnss.me"
gitea_ssh_domain: "jnss.me"
# Result: git@jnss.me:user/repo.git
# Same domain for both
gitea_http_domain: "git.jnss.me"
gitea_ssh_domain: "git.jnss.me"
# Result: git@git.jnss.me:user/repo.git
# Completely custom
gitea_http_domain: "code.jnss.me"
gitea_ssh_domain: "git.example.com"
# Result: git@git.example.com:user/repo.git
```
## SSH Modes
### Passthrough Mode (Default - Recommended)
System SSH handles Git operations via AuthorizedKeysCommand:
```bash
# Clone repository (standard Git URL, no port number)
git clone git@jnss.me:username/repository.git
# Add as remote
git remote add origin git@jnss.me:username/repository.git
# Test SSH connection
ssh -T git@jnss.me
```
**Features:**
- ✅ Standard Git URLs (no :2222 port)
- ✅ Single SSH daemon (smaller attack surface)
- ✅ System fail2ban protects everything
- ✅ Port 22 only (no extra firewall rules)
- ✅ Matches GitHub/GitLab pattern
### Dedicated Mode (Fallback)
Gitea runs its own SSH server on port 2222:
```bash
# Clone repository (with port number)
git clone ssh://git@jnss.me:2222/username/repository.git
# Add as remote
git remote add origin ssh://git@jnss.me:2222/username/repository.git
# Test SSH connection
ssh -T -p 2222 git@jnss.me
```
**Features:**
- ✅ Complete isolation from system SSH
- ✅ Independent configuration
- ⚠️ Requires port 2222 open in firewall
- ⚠️ Non-standard URLs (requires :2222)
**To switch modes:**
```yaml
# host_vars/arch-vps/main.yml
gitea_ssh_mode: "dedicated" # or "passthrough"
```
Then re-run the playbook.
## Usage ## Usage
1. **Add vault password**: Set `vault_gitea_db_password` in host_vars vault 1. **Add vault password**: Set `vault_gitea_db_password` in host_vars vault
2. **Deploy**: `ansible-playbook site.yml --tags gitea` 2. **Configure domains** (optional): Override `gitea_http_domain` and `gitea_ssh_domain` in host_vars
3. **Access**: Visit https://git.yourdomain.com to set up admin account 3. **Deploy**: `ansible-playbook site.yml --tags gitea`
4. **Access**: Visit https://git.jnss.me to set up admin account
## SSH Key Setup
1. **Generate SSH key** (if you don't have one):
```bash
ssh-keygen -t ed25519 -C "your_email@example.com"
```
2. **Copy your public key**:
```bash
cat ~/.ssh/id_ed25519.pub
```
3. **Add to Gitea**:
- Log into Gitea web interface
- Go to Settings → SSH/GPG Keys
- Click "Add Key"
- Paste your public key
4. **Test SSH connection**:
```bash
# Passthrough mode
ssh -T git@jnss.me
# Dedicated mode
ssh -T -p 2222 git@jnss.me
```
## Firewall and Security
### Automatic Firewall Management
Firewall configuration is mode-aware:
**Passthrough Mode:**
- No extra firewall rules needed (uses port 22)
- System SSH already configured in security playbook
**Dedicated Mode:**
- Port 2222 automatically opened via nftables
- Firewall rules stored in `/etc/nftables.d/50-gitea.nft`
- Rules integrated with main security playbook
- Automatically removed when switching to passthrough
### fail2ban Protection
Protection is mode-aware:
**Passthrough Mode:**
- System `sshd` jail protects all SSH traffic (port 22)
- Covers admin SSH + Git operations automatically
- No separate Gitea jail needed
**Dedicated Mode:**
- `gitea-ssh` jail monitors Gitea logs (port 2222)
- Max retries: 5 failed attempts
- Find time: 10 minutes (600 seconds)
- Ban time: 1 hour (3600 seconds)
- Action: IP banned via nftables
Check fail2ban status:
```bash
# Passthrough mode
fail2ban-client status sshd
# Dedicated mode
fail2ban-client status gitea-ssh
```
### Firewall Verification
```bash
# List active nftables rules
nft list ruleset
# Check for Gitea SSH port (should be empty in passthrough)
nft list ruleset | grep 2222
# Verify SSH connectivity
nc -zv jnss.me 22 # Passthrough
nc -zv jnss.me 2222 # Dedicated
```
## Dependencies ## Dependencies
@@ -62,9 +245,19 @@ This role follows rick-infra's self-contained service pattern:
- Creates its own database and user via PostgreSQL infrastructure - Creates its own database and user via PostgreSQL infrastructure
- Manages its own configuration and data - Manages its own configuration and data
- Deploys its own Caddy reverse proxy config - Deploys its own Caddy reverse proxy config
- Manages its own firewall rules and security (nftables, fail2ban)
- Flexible domain configuration (not tied to infrastructure variables)
- Independent lifecycle from other services - Independent lifecycle from other services
## Migration Guide
See `docs/gitea-ssh-migration-guide.md` for:
- Switching between SSH modes
- Updating Git remote URLs
- Bulk migration scripts
- Troubleshooting
--- ---
**Rick-Infra Gitea Service** **Rick-Infra Gitea Service**
Git repository management with integrated CI/CD capabilities. Git repository management with flexible SSH modes and domain configuration.

View File

@@ -20,16 +20,14 @@ gitea_home: "/var/lib/gitea"
# Network Configuration # Network Configuration
gitea_http_port: 3000 gitea_http_port: 3000
gitea_ssh_port: 2222
# ================================================================= # =================================================================
# Domain and Caddy Integration # Domain and Caddy Integration
# ================================================================= # =================================================================
# Domain setup (follows rick-infra pattern) # Domain setup (follows rick-infra pattern)
gitea_subdomain: "git" gitea_http_domain: "git.jnss.me"
gitea_domain: "{{ caddy_domain | default('localhost') }}" gitea_ssh_domain: "jnss.me"
gitea_full_domain: "{{ gitea_subdomain }}.{{ gitea_domain }}"
# Caddy integration # Caddy integration
caddy_sites_enabled_dir: "/etc/caddy/sites-enabled" caddy_sites_enabled_dir: "/etc/caddy/sites-enabled"
@@ -59,12 +57,129 @@ gitea_run_mode: "prod"
gitea_default_branch: "main" gitea_default_branch: "main"
gitea_enable_lfs: true gitea_enable_lfs: true
# Security settings # =================================================================
gitea_disable_registration: false # Private Git Server & OAuth Configuration
gitea_require_signin: false # =================================================================
# SSH settings # Access Control - Private server with public repos allowed
gitea_start_ssh_server: true gitea_disable_registration: true # No public registration (admin only)
gitea_require_signin: false # Require sign-in (unauthorized users read-only)
gitea_show_registration_button: false # Hide registration UI
# OAuth Configuration - Preferred but not forced
gitea_enable_password_signin: false # Hide password login form
gitea_enable_basic_auth: true # Keep password API auth as backup
gitea_oauth2_auto_registration: true # Auto-create OAuth users
gitea_oauth2_account_linking: "login" # Show account linking page
gitea_oauth2_username_source: "preferred_username"
gitea_oauth2_update_avatar: true
gitea_oauth2_scopes: "profile,email,groups"
gitea_oauth2_register_email_confirm: false
# =================================================================
# Email Configuration (Titan Email via Hostinger)
# =================================================================
gitea_mailer_enabled: true
gitea_mailer_protocol: "smtp+starttls" # Port 587 with STARTTLS
gitea_smtp_addr: "smtp.titan.email"
gitea_smtp_port: 587
gitea_mailer_from: "hello@jnss.me"
gitea_mailer_user: "hello@jnss.me"
gitea_mailer_password: "{{ vault_smtp_password }}"
gitea_mailer_subject_prefix: "[Gitea]"
# =================================================================
# Enhanced Security Settings
# =================================================================
# Session Security
gitea_session_provider: "file"
gitea_session_cookie_name: "gitea_session"
gitea_session_life_time: 3600 # 1 hour
gitea_cookie_secure: true # HTTPS-only cookies
gitea_session_same_site: "strict" # Strict CSRF protection
# Security Hardening
gitea_csrf_cookie_httponly: true # Prevent XSS on CSRF token
gitea_password_check_pwn: true # Check password breach database
gitea_reverse_proxy_limit: 1 # Trust only one proxy (Caddy)
gitea_reverse_proxy_trusted_proxies: "127.0.0.0/8,::1/128"
# =================================================================
# Repository Configuration
# =================================================================
# Privacy Defaults (private by default, public allowed)
gitea_default_private: "private" # New repos are private
gitea_default_push_create_private: true # Push-created repos are private
# Note: NOT setting gitea_force_private - allows public repos
# Repository Features
gitea_disabled_repo_units: "repo.ext_issues,repo.ext_wiki"
gitea_enable_push_create_user: false # Require manual repo creation
gitea_enable_push_create_org: false
# =================================================================
# Features & Capabilities
# =================================================================
# CI/CD Actions
gitea_actions_enabled: true # Enable Gitea Actions
gitea_actions_default_url: "github" # Use GitHub actions
gitea_actions_log_retention_days: 90
gitea_actions_artifact_retention_days: 30
# Repository Mirroring
gitea_mirror_enabled: true
gitea_mirror_default_interval: "8h"
gitea_mirror_min_interval: "1h"
# Organization & User Management
gitea_allow_create_org: true # Users can create orgs
# API Configuration
gitea_api_swagger_enabled: false # Disable API docs
# Webhook Security
gitea_webhook_allowed_hosts: "private,loopback"
gitea_webhook_skip_tls_verify: false
gitea_webhook_deliver_timeout: 5
# =================================================================
# Service Explore Configuration
# =================================================================
gitea_explore_require_signin: false # Allow browsing public content
# =================================================================
# SSH Mode Configuration
# =================================================================
# SSH Mode: 'passthrough' or 'dedicated'
# - passthrough (default): Use system SSH on port 22
# * More secure (single SSH daemon, smaller attack surface)
# * Standard Git URLs (no :2222 port number needed)
# * System fail2ban automatically protects Git operations
# * Recommended for production use
#
# - dedicated (fallback): Run Gitea's built-in SSH server on port 2222
# * Complete isolation from system SSH
# * Independent configuration and restarts
# * Requires opening port 2222 in firewall
# * Useful for debugging or when passthrough causes issues
gitea_ssh_mode: "passthrough"
# Dynamic SSH configuration based on mode
gitea_ssh_port: "{{ 22 if gitea_ssh_mode == 'passthrough' else 2222 }}"
gitea_start_ssh_server: "{{ false if gitea_ssh_mode == 'passthrough' else true }}"
# =================================================================
# Firewall Configuration
# =================================================================
# Firewall management (only opens port in dedicated mode)
gitea_manage_firewall: "{{ true if gitea_ssh_mode == 'dedicated' else false }}"
# ================================================================= # =================================================================
# Infrastructure Dependencies (Read-only) # Infrastructure Dependencies (Read-only)

View File

@@ -16,3 +16,22 @@
name: caddy name: caddy
state: reloaded state: reloaded
when: caddy_service_enabled | default(false) when: caddy_service_enabled | default(false)
- name: reload nftables
systemd:
name: nftables
state: reloaded
# Safety: only reload if service is active
when: ansible_connection != 'local'
- name: restart fail2ban
systemd:
name: fail2ban
state: restarted
- name: restart sshd
systemd:
name: sshd
state: restarted
# Safety: only restart if not running locally
when: ansible_connection != 'local'

View File

@@ -0,0 +1,121 @@
---
# Gitea fail2ban Configuration - Rick-Infra
# Mode-aware: Only protects dedicated mode (port 2222)
# In passthrough mode, system 'sshd' jail protects port 22
- name: Install fail2ban
pacman:
name: fail2ban
state: present
- name: Create Gitea fail2ban filter
copy:
content: |
# Fail2ban filter for Gitea SSH authentication failures
# Rick-Infra: Gitea role
# Only used in dedicated mode (port {{ gitea_ssh_port }})
[Definition]
# Match failed authentication attempts in Gitea logs
failregex = .*(Failed authentication attempt|authentication failed|Invalid user|Failed login attempt).*from\s+<HOST>
.*level=warning.*msg=.*authentication.*failed.*ip=<HOST>
ignoreregex =
dest: /etc/fail2ban/filter.d/gitea-ssh.conf
mode: '0644'
backup: yes
notify: restart fail2ban
- name: Ensure fail2ban jail.local exists
file:
path: /etc/fail2ban/jail.local
state: touch
mode: '0644'
modification_time: preserve
access_time: preserve
- name: Add Gitea SSH jail to fail2ban (mode-aware)
blockinfile:
path: /etc/fail2ban/jail.local
marker: "# {mark} ANSIBLE MANAGED BLOCK - Gitea SSH"
block: |
# Gitea SSH Protection - Rick-Infra
# Mode: {{ gitea_ssh_mode }}
# - dedicated: Monitors Gitea logs on port {{ gitea_ssh_port }}
# - passthrough: Disabled (system 'sshd' jail protects port 22)
[gitea-ssh]
enabled = {{ 'true' if gitea_ssh_mode == 'dedicated' else 'false' }}
port = {{ gitea_ssh_port }}
filter = gitea-ssh
logpath = {{ gitea_home }}/log/gitea.log
maxretry = 5
findtime = 600
bantime = 3600
banaction = nftables
backup: yes
notify: restart fail2ban
- name: Enable and start fail2ban service
systemd:
name: fail2ban
enabled: yes
state: started
- name: Flush handlers to ensure fail2ban restarts
meta: flush_handlers
- name: Wait for fail2ban to be ready
pause:
seconds: 2
- name: Verify gitea-ssh jail status (dedicated mode only)
command: fail2ban-client status gitea-ssh
register: gitea_jail_verify
changed_when: false
failed_when: false
when: gitea_ssh_mode == 'dedicated'
- name: Verify sshd jail status (passthrough mode)
command: fail2ban-client status sshd
register: sshd_jail_verify
changed_when: false
failed_when: false
when: gitea_ssh_mode == 'passthrough'
- name: Display fail2ban configuration status
debug:
msg: |
🛡️ fail2ban Protection for Gitea SSH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📍 Mode: {{ gitea_ssh_mode | upper }}
{% if gitea_ssh_mode == 'dedicated' %}
📍 Jail: gitea-ssh
📍 Port: {{ gitea_ssh_port }}
📍 Status: {{ 'Active ✅' if gitea_jail_verify.rc == 0 else 'Not Active ⚠️' }}
📍 Filter: /etc/fail2ban/filter.d/gitea-ssh.conf
📍 Logfile: {{ gitea_home }}/log/gitea.log
Protection Settings:
• Max retries: 5 attempts
• Find time: 10 minutes (600 seconds)
• Ban time: 1 hour (3600 seconds)
Check status:
fail2ban-client status gitea-ssh
{% else %}
📍 Jail: sshd (system jail)
📍 Port: 22
📍 Status: {{ 'Active ✅' if sshd_jail_verify.rc == 0 else 'Not Active ⚠️' }}
📍 Coverage: All SSH traffic including Gitea Git operations
Note: In passthrough mode, the system 'sshd' jail automatically
protects all SSH traffic on port 22, including Gitea Git
operations. No separate gitea-ssh jail is needed.
Check status:
fail2ban-client status sshd
{% endif %}
# Rick-Infra: Self-contained fail2ban protection per role

View File

@@ -0,0 +1,51 @@
---
# Gitea Firewall Configuration - Rick-Infra
# Self-contained firewall management for Gitea SSH access
# Opens port 2222 for Gitea's SSH server
- name: Install nftables (if not present)
pacman:
name: nftables
state: present
- name: Create nftables rules directory
file:
path: /etc/nftables.d
state: directory
mode: '0755'
- name: Deploy Gitea nftables rules
template:
src: gitea.nft.j2
dest: /etc/nftables.d/50-gitea.nft
mode: '0644'
notify: reload nftables
register: gitea_nft_deployed
- name: Validate nftables loader configuration
command: nft -c -f /etc/nftables-load.conf
changed_when: false
failed_when: false
register: nft_validation
- name: Display nftables validation results
debug:
msg: "{{ 'nftables configuration valid' if nft_validation.rc == 0 else 'nftables validation failed: ' + nft_validation.stderr }}"
when: nft_validation is defined
- name: Enable and start nftables service
systemd:
name: nftables
enabled: yes
state: started
- name: Display Gitea firewall status
debug:
msg: |
🔥 Gitea firewall configuration deployed
📍 Rule file: /etc/nftables.d/50-gitea.nft
🔓 Port opened: {{ gitea_ssh_port }} (Gitea SSH)
⚠️ Note: nftables will reload automatically via handler
# Rick-Infra: Self-contained firewall management per role

View File

@@ -16,11 +16,30 @@
name: gitea name: gitea
state: present state: present
# SSH Mode Configuration - Conditional based on gitea_ssh_mode
# Mode determines how Git SSH operations are handled
- name: Configure SSH passthrough mode (default)
import_tasks: ssh_passthrough.yml
when: gitea_ssh_mode == "passthrough"
tags: ['ssh', 'passthrough']
- name: Configure SSH dedicated mode (fallback)
import_tasks: ssh_dedicated.yml
when: gitea_ssh_mode == "dedicated"
tags: ['ssh', 'dedicated']
- name: Install Git - name: Install Git
pacman: pacman:
name: git name: git
state: present state: present
- name: Create Gitea group
group:
name: "{{ gitea_group }}"
system: yes
state: present
- name: Create Gitea user and group - name: Create Gitea user and group
user: user:
name: "{{ gitea_user }}" name: "{{ gitea_user }}"
@@ -144,8 +163,8 @@
msg: | msg: |
✅ Gitea Git service deployed successfully! ✅ Gitea Git service deployed successfully!
🌐 Web Interface: https://{{ gitea_full_domain }} 🌐 Web Interface: https://{{ gitea_http_domain }}
🔗 SSH Clone: ssh://git@{{ gitea_full_domain }}:{{ gitea_ssh_port }} 🔗 SSH Clone: ssh://git@{{ gitea_ssh_domain }}:{{ gitea_ssh_port }}
📦 Local HTTP: http://127.0.0.1:{{ gitea_http_port }} 📦 Local HTTP: http://127.0.0.1:{{ gitea_http_port }}
🗄️ Database: {{ gitea_db_name }} (self-managed) 🗄️ Database: {{ gitea_db_name }} (self-managed)

View File

@@ -0,0 +1,74 @@
---
# Gitea Dedicated SSH Server Configuration - Rick-Infra
# Configures Gitea to run its own SSH server on port 2222
# This is the fallback mode when passthrough is not desired
- name: Configure firewall for Gitea SSH (dedicated mode)
import_tasks: firewall.yml
tags: ['firewall']
- name: Configure fail2ban for Gitea SSH (dedicated mode)
import_tasks: fail2ban.yml
tags: ['fail2ban', 'security']
- name: Wait for fail2ban to be ready
pause:
seconds: 2
- name: Verify gitea-ssh jail is active
command: fail2ban-client status gitea-ssh
register: gitea_jail_status
changed_when: false
failed_when: false
- name: Display fail2ban protection status
debug:
msg: |
🛡️ Gitea SSH fail2ban protection:
{% if gitea_jail_status.rc == 0 %}
✅ gitea-ssh jail is ACTIVE
{{ gitea_jail_status.stdout }}
{% else %}
⚠️ WARNING: gitea-ssh jail not active!
This is a security risk - port {{ gitea_ssh_port }} is vulnerable to brute force attacks.
{% endif %}
- name: Fail if gitea-ssh jail is not running (security critical)
fail:
msg: |
SECURITY ERROR: gitea-ssh fail2ban jail is not active!
Port {{ gitea_ssh_port }} is exposed but not protected.
Check fail2ban configuration and logs.
when: gitea_jail_status.rc != 0
- name: Remove SSH passthrough configuration if present
blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} ANSIBLE MANAGED BLOCK - Gitea SSH Passthrough"
state: absent
backup: yes
register: sshd_config_cleaned
notify: restart sshd
- name: Remove AuthorizedKeysCommand script if present
file:
path: /usr/local/bin/gitea-keys
state: absent
- name: Display dedicated mode configuration
debug:
msg: |
🔧 Gitea SSH Mode: DEDICATED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📍 SSH Server: Gitea built-in (port {{ gitea_ssh_port }})
🔗 Clone URL: ssh://git@{{ gitea_ssh_domain }}:{{ gitea_ssh_port }}/user/repo.git
🔥 Firewall: Port {{ gitea_ssh_port }} opened (nftables)
🛡️ fail2ban: gitea-ssh jail protecting port {{ gitea_ssh_port }}
Test connection:
ssh -T -p {{ gitea_ssh_port }} git@{{ gitea_ssh_domain }}
Clone repository:
git clone ssh://git@{{ gitea_ssh_domain }}:{{ gitea_ssh_port }}/username/repo.git
# Rick-Infra: Self-contained dedicated SSH mode with full security

View File

@@ -0,0 +1,128 @@
---
# Gitea SSH Passthrough Configuration - Rick-Infra
# Configures system SSH to handle Gitea Git authentication
# This is the default mode: more secure, standard Git URLs
- name: Ensure OpenSSH server is installed
pacman:
name: openssh
state: present
- name: Create Gitea AuthorizedKeysCommand script
template:
src: gitea-keys.sh.j2
dest: /usr/local/bin/gitea-keys
mode: '0755'
owner: root
group: root
register: gitea_keys_script
- name: Configure SSH for Gitea passthrough
blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} ANSIBLE MANAGED BLOCK - Gitea SSH Passthrough"
block: |
# Gitea SSH Passthrough - Rick-Infra
# System SSH delegates git user authentication to Gitea
# This allows standard Git URLs: git@{{ gitea_ssh_domain }}:user/repo.git
Match User {{ gitea_user }}
AuthorizedKeysCommandUser {{ gitea_user }}
AuthorizedKeysCommand /usr/local/bin/gitea-keys %u %t %k
AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no
PermitTTY no
backup: yes
validate: '/usr/bin/sshd -t -f %s'
register: sshd_config_changed
notify: restart sshd
- name: Verify SSH configuration syntax
command: sshd -t
changed_when: false
register: sshd_test
- name: Display SSH validation result
debug:
msg: |
{% if sshd_test.rc == 0 %}
✅ SSH configuration is valid
{% else %}
⚠️ SSH configuration test failed:
{{ sshd_test.stderr }}
{% endif %}
- name: Fail if SSH configuration is invalid
fail:
msg: "SSH configuration test failed. Rolling back changes."
when: sshd_test.rc != 0
- name: Remove Gitea firewall rule (passthrough uses port 22)
file:
path: /etc/nftables.d/50-gitea.nft
state: absent
notify: reload nftables
register: firewall_cleaned
- name: Reload nftables if firewall rule was removed
systemd:
name: nftables
state: reloaded
when: firewall_cleaned.changed
- name: Configure fail2ban for passthrough mode
import_tasks: fail2ban.yml
tags: ['fail2ban', 'security']
- name: Flush handlers to ensure sshd restarts if needed
meta: flush_handlers
- name: Wait for SSH service to be available after restart
wait_for:
port: 22
host: "{{ ansible_host | default('127.0.0.1') }}"
timeout: 30
delegate_to: localhost
become: false
when: sshd_config_changed.changed
- name: Test SSH connection after configuration
ping:
when: sshd_config_changed.changed
- name: Verify passthrough is working
command: sudo -u {{ gitea_user }} /usr/local/bin/gitea-keys ssh-rsa test
register: gitea_keys_test
changed_when: false
failed_when: false
- name: Display passthrough mode configuration
debug:
msg: |
🔧 Gitea SSH Mode: PASSTHROUGH (Default)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📍 SSH Server: System SSH (port 22)
🔗 Clone URL: git@{{ gitea_ssh_domain }}:user/repo.git
🔥 Firewall: Port 2222 closed (not needed)
🛡️ fail2ban: System 'sshd' jail protects all SSH traffic
🔑 AuthorizedKeysCommand: /usr/local/bin/gitea-keys
How it works:
1. User connects: ssh git@{{ gitea_ssh_domain }}
2. System SSH checks: /usr/local/bin/gitea-keys
3. Script queries Gitea database for SSH key
4. If authorized, Gitea handles Git operation
Test connection:
ssh -T git@{{ gitea_ssh_domain }}
Clone repository:
git clone git@{{ gitea_ssh_domain }}:username/repo.git
Benefits:
✅ Standard Git URLs (no :2222 port number)
✅ Single SSH daemon (smaller attack surface)
✅ System fail2ban protects everything
✅ One port to manage and monitor
# Rick-Infra: Self-contained SSH passthrough mode with enhanced security

View File

@@ -6,19 +6,32 @@ APP_NAME = {{ gitea_app_name }}
RUN_MODE = {{ gitea_run_mode }} RUN_MODE = {{ gitea_run_mode }}
[repository] [repository]
# === Repository Storage ===
ROOT = {{ gitea_home }}/repositories ROOT = {{ gitea_home }}/repositories
DEFAULT_BRANCH = {{ gitea_default_branch }} DEFAULT_BRANCH = {{ gitea_default_branch }}
# === Privacy Defaults ===
DEFAULT_PRIVATE = {{ gitea_default_private }}
DEFAULT_PUSH_CREATE_PRIVATE = {{ gitea_default_push_create_private | lower }}
# === Repository Features ===
DISABLED_REPO_UNITS = {{ gitea_disabled_repo_units }}
ENABLE_PUSH_CREATE_USER = {{ gitea_enable_push_create_user | lower }}
ENABLE_PUSH_CREATE_ORG = {{ gitea_enable_push_create_org | lower }}
[server] [server]
PROTOCOL = http PROTOCOL = http
DOMAIN = {{ gitea_full_domain }} DOMAIN = {{ gitea_http_domain }}
HTTP_PORT = {{ gitea_http_port }} HTTP_PORT = {{ gitea_http_port }}
ROOT_URL = https://{{ gitea_full_domain }}/ ROOT_URL = https://{{ gitea_http_domain }}/
DISABLE_SSH = false DISABLE_SSH = false
# SSH Mode: {{ gitea_ssh_mode }}
START_SSH_SERVER = {{ gitea_start_ssh_server | lower }} START_SSH_SERVER = {{ gitea_start_ssh_server | lower }}
SSH_DOMAIN = {{ gitea_full_domain }} SSH_DOMAIN = {{ gitea_ssh_domain }}
SSH_PORT = {{ gitea_ssh_port }} SSH_PORT = {{ gitea_ssh_port }}
{% if gitea_ssh_mode == 'dedicated' %}
SSH_LISTEN_PORT = {{ gitea_ssh_port }} SSH_LISTEN_PORT = {{ gitea_ssh_port }}
{% endif %}
LOCAL_ROOT_URL = http://127.0.0.1:{{ gitea_http_port }}/ LOCAL_ROOT_URL = http://127.0.0.1:{{ gitea_http_port }}/
APP_DATA_PATH = {{ gitea_home }}/data APP_DATA_PATH = {{ gitea_home }}/data
@@ -38,16 +51,62 @@ SSL_MODE = disable
CHARSET = utf8 CHARSET = utf8
[security] [security]
# === Core Security ===
INSTALL_LOCK = true INSTALL_LOCK = true
SECRET_KEY = {{ ansible_machine_id }}{{ gitea_db_password | hash('sha256') }} SECRET_KEY = {{ ansible_machine_id }}{{ gitea_db_password | hash('sha256') }}
INTERNAL_TOKEN = {{ (ansible_machine_id + gitea_db_password) | hash('sha256') }} INTERNAL_TOKEN = {{ (ansible_machine_id + gitea_db_password) | hash('sha256') }}
# === Enhanced Security ===
CSRF_COOKIE_HTTP_ONLY = {{ gitea_csrf_cookie_httponly | lower }}
PASSWORD_CHECK_PWN = {{ gitea_password_check_pwn | lower }}
REVERSE_PROXY_LIMIT = {{ gitea_reverse_proxy_limit }}
REVERSE_PROXY_TRUSTED_PROXIES = {{ gitea_reverse_proxy_trusted_proxies }}
[service] [service]
# === Access Control ===
DISABLE_REGISTRATION = {{ gitea_disable_registration | lower }} DISABLE_REGISTRATION = {{ gitea_disable_registration | lower }}
REQUIRE_SIGNIN_VIEW = {{ gitea_require_signin | lower }} REQUIRE_SIGNIN_VIEW = {{ gitea_require_signin | lower }}
SHOW_REGISTRATION_BUTTON = {{ gitea_show_registration_button | lower }}
# === OAuth Configuration ===
ENABLE_PASSWORD_SIGNIN_FORM = {{ gitea_enable_password_signin | lower }}
ENABLE_BASIC_AUTHENTICATION = {{ gitea_enable_basic_auth | lower }}
# === Defaults ===
DEFAULT_KEEP_EMAIL_PRIVATE = true DEFAULT_KEEP_EMAIL_PRIVATE = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = true DEFAULT_ALLOW_CREATE_ORGANIZATION = {{ gitea_allow_create_org | lower }}
NO_REPLY_ADDRESS = noreply.{{ gitea_domain }} NO_REPLY_ADDRESS = noreply@{{ gitea_http_domain }}
[oauth2_client]
# === Authentik OAuth Integration ===
ENABLE_AUTO_REGISTRATION = {{ gitea_oauth2_auto_registration | lower }}
ACCOUNT_LINKING = {{ gitea_oauth2_account_linking }}
USERNAME = {{ gitea_oauth2_username_source }}
UPDATE_AVATAR = {{ gitea_oauth2_update_avatar | lower }}
OPENID_CONNECT_SCOPES = {{ gitea_oauth2_scopes }}
REGISTER_EMAIL_CONFIRM = {{ gitea_oauth2_register_email_confirm | lower }}
[mailer]
ENABLED = {{ gitea_mailer_enabled | lower }}
{% if gitea_mailer_enabled %}
PROTOCOL = {{ gitea_mailer_protocol }}
SMTP_ADDR = {{ gitea_smtp_addr }}
SMTP_PORT = {{ gitea_smtp_port }}
FROM = {{ gitea_mailer_from }}
USER = {{ gitea_mailer_user }}
PASSWD = {{ gitea_mailer_password }}
SUBJECT_PREFIX = {{ gitea_mailer_subject_prefix }}
SEND_AS_PLAIN_TEXT = false
SMTP_AUTH = PLAIN
{% endif %}
[session]
# === Session Security ===
PROVIDER = {{ gitea_session_provider }}
COOKIE_NAME = {{ gitea_session_cookie_name }}
COOKIE_SECURE = {{ gitea_cookie_secure | lower }}
SESSION_LIFE_TIME = {{ gitea_session_life_time }}
SAME_SITE = {{ gitea_session_same_site }}
[log] [log]
MODE = console MODE = console
@@ -63,4 +122,37 @@ CONTENT_PATH = {{ gitea_home }}/data/lfs
[git] [git]
PATH = /usr/bin/git PATH = /usr/bin/git
# Rick-Infra: Simplified Gitea configuration for self-contained service [actions]
# === CI/CD Configuration ===
ENABLED = {{ gitea_actions_enabled | lower }}
{% if gitea_actions_enabled %}
DEFAULT_ACTIONS_URL = {{ gitea_actions_default_url }}
LOG_RETENTION_DAYS = {{ gitea_actions_log_retention_days }}
ARTIFACT_RETENTION_DAYS = {{ gitea_actions_artifact_retention_days }}
{% endif %}
[mirror]
# === Repository Mirroring ===
ENABLED = {{ gitea_mirror_enabled | lower }}
DISABLE_NEW_PULL = false
DISABLE_NEW_PUSH = false
DEFAULT_INTERVAL = {{ gitea_mirror_default_interval }}
MIN_INTERVAL = {{ gitea_mirror_min_interval }}
[api]
# === API Configuration ===
ENABLE_SWAGGER = {{ gitea_api_swagger_enabled | lower }}
MAX_RESPONSE_ITEMS = 50
DEFAULT_PAGING_NUM = 30
[webhook]
# === Webhook Security ===
ALLOWED_HOST_LIST = {{ gitea_webhook_allowed_hosts }}
SKIP_TLS_VERIFY = {{ gitea_webhook_skip_tls_verify | lower }}
DELIVER_TIMEOUT = {{ gitea_webhook_deliver_timeout }}
[service.explore]
# === Public Content Exploration ===
REQUIRE_SIGNIN_VIEW = {{ gitea_explore_require_signin | lower }}
# Rick-Infra: Private Gitea configuration with OAuth and email support

View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Gitea SSH Keys AuthorizedKeysCommand - Rick-Infra
# Generated by Ansible Gitea role
#
# This script is called by OpenSSH's AuthorizedKeysCommand to query
# Gitea's database for SSH public keys when the 'git' user connects.
#
# Called by SSH with parameters:
# %u = username (should be "git")
# %t = key type (ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256, etc.)
# %k = base64 encoded public key content
#
# The script returns authorized_keys format entries that include
# forced commands to execute Gitea's Git server.
set -euo pipefail
# Gitea keys command queries the database and returns authorized_keys format
# If the key is found, it returns a line like:
# command="/usr/bin/gitea serv key-123",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA...
exec /usr/bin/gitea keys \
--config /etc/gitea/app.ini \
--username "$1" \
--type "$2" \
--content "$3"
# Rick-Infra: AuthorizedKeysCommand for Gitea SSH passthrough mode

View File

@@ -2,7 +2,7 @@
# Generated by Ansible Gitea role # Generated by Ansible Gitea role
# Deployed to {{ caddy_sites_enabled_dir }}/gitea.caddy # Deployed to {{ caddy_sites_enabled_dir }}/gitea.caddy
{{ gitea_full_domain }} { {{ gitea_http_domain }} {
# Reverse proxy to Gitea # Reverse proxy to Gitea
reverse_proxy 127.0.0.1:{{ gitea_http_port }} reverse_proxy 127.0.0.1:{{ gitea_http_port }}

View File

@@ -0,0 +1,11 @@
# Gitea SSH Firewall Rules - Rick-Infra
# Generated by Ansible Gitea role
# Allows incoming SSH connections on port {{ gitea_ssh_port }}
#
# This file is loaded BEFORE the final drop rule (99-drop.nft)
# Filename: 50-gitea.nft (ensures proper load order)
# Add Gitea SSH port to the input chain
add rule inet filter input tcp dport {{ gitea_ssh_port }} ct state new accept comment "Gitea SSH (Port {{ gitea_ssh_port }})"
# Rick-Infra: Self-contained firewall rule for Gitea SSH access

325
roles/metrics/README.md Normal file
View File

@@ -0,0 +1,325 @@
# Metrics Role
Complete monitoring stack for rick-infra providing system metrics collection, storage, and visualization with SSO integration.
## Components
### VictoriaMetrics
- **Purpose**: Time-series database for metrics storage
- **Type**: Native systemd service
- **Listen**: `127.0.0.1:8428` (localhost only)
- **Features**:
- Prometheus-compatible API and PromQL
- 7x less RAM usage than Prometheus
- Single binary deployment
- 12-month data retention by default
### Grafana
- **Purpose**: Metrics visualization and dashboarding
- **Type**: Native systemd service
- **Listen**: `127.0.0.1:3000` (localhost only, proxied via Caddy)
- **Domain**: `metrics.jnss.me`
- **Features**:
- OAuth/OIDC integration with Authentik
- Role-based access control via Authentik groups
- VictoriaMetrics as default data source
### node_exporter
- **Purpose**: System metrics collection
- **Type**: Native systemd service
- **Listen**: `127.0.0.1:9100` (localhost only)
- **Metrics**: CPU, memory, disk, network, systemd units
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ metrics.jnss.me (Grafana Dashboard) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Caddy (HTTPS) │ │
│ │ ↓ │ │
│ │ Grafana (OAuth → Authentik) │ │
│ │ ↓ │ │
│ │ VictoriaMetrics (Prometheus-compatible) │ │
│ │ ↑ │ │
│ │ node_exporter (System Metrics) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
## Deployment
### Prerequisites
1. **Caddy role deployed** - Required for HTTPS proxy
2. **Authentik deployed** - Required for OAuth/SSO
3. **Vault variables configured**:
```yaml
# In host_vars/arch-vps/vault.yml
vault_grafana_admin_password: "secure-admin-password"
vault_grafana_secret_key: "random-secret-key-32-chars"
vault_grafana_oauth_client_id: "grafana"
vault_grafana_oauth_client_secret: "oauth-client-secret-from-authentik"
```
### Authentik Configuration
Before deployment, create OAuth2/OIDC provider in Authentik:
1. **Create Provider**:
- Name: `Grafana`
- Type: `OAuth2/OpenID Provider`
- Client ID: `grafana`
- Client Secret: Generate and save to vault
- Redirect URIs: `https://metrics.jnss.me/login/generic_oauth`
- Signing Key: Auto-generated
2. **Create Application**:
- Name: `Grafana`
- Slug: `grafana`
- Provider: Select Grafana provider created above
3. **Create Groups** (optional, for role mapping):
- `grafana-admins` - Full admin access
- `grafana-editors` - Can create/edit dashboards
- Users without these groups get Viewer access
### Deploy
```bash
# Deploy complete metrics stack
ansible-playbook rick-infra.yml --tags metrics
# Deploy individual components
ansible-playbook rick-infra.yml --tags victoriametrics
ansible-playbook rick-infra.yml --tags grafana
ansible-playbook rick-infra.yml --tags node_exporter
```
### Verify Deployment
```bash
# Check service status
ansible homelab -a "systemctl status victoriametrics grafana node_exporter"
# Check metrics collection
curl http://127.0.0.1:9100/metrics # node_exporter metrics
curl http://127.0.0.1:8428/metrics # VictoriaMetrics metrics
curl http://127.0.0.1:8428/api/v1/targets # Scrape targets
# Access Grafana
curl -I https://metrics.jnss.me/ # Should redirect to Authentik login
```
## Usage
### Access Dashboard
1. Navigate to `https://metrics.jnss.me`
2. Click "Sign in with Authentik"
3. Authenticate via Authentik SSO
4. Access granted based on Authentik group membership
### Role Mapping
Grafana roles are automatically assigned based on Authentik groups:
- **Admin**: Members of `grafana-admins` group
- Full administrative access
- Can manage users, data sources, plugins
- Can create/edit/delete all dashboards
- **Editor**: Members of `grafana-editors` group
- Can create and edit dashboards
- Cannot manage users or data sources
- **Viewer**: All other authenticated users
- Read-only access to dashboards
- Cannot create or edit dashboards
### Creating Dashboards
Grafana comes with VictoriaMetrics pre-configured as the default data source. Use PromQL queries:
```promql
# CPU usage
100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# Memory usage
node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes
# Disk usage
100 - ((node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100)
# Network traffic
irate(node_network_receive_bytes_total[5m])
```
### Import Community Dashboards
1. Browse dashboards at https://grafana.com/grafana/dashboards/
2. Recommended for node_exporter:
- Dashboard ID: 1860 (Node Exporter Full)
- Dashboard ID: 11074 (Node Exporter for Prometheus)
3. Import via Grafana UI: Dashboards → Import → Enter ID
## Configuration
### Customization
Key configuration options in `roles/metrics/defaults/main.yml`:
```yaml
# Data retention
victoriametrics_retention_period: "12" # months
# Scrape interval
victoriametrics_scrape_interval: "15s"
# OAuth role mapping (JMESPath expression)
grafana_oauth_role_attribute_path: "contains(groups, 'grafana-admins') && 'Admin' || contains(groups, 'grafana-editors') && 'Editor' || 'Viewer'"
# Memory limits
victoriametrics_memory_allowed_percent: "60"
```
### Adding Scrape Targets
Edit `roles/metrics/templates/scrape.yml.j2`:
```yaml
scrape_configs:
# Add custom application metrics
- job_name: 'myapp'
static_configs:
- targets: ['127.0.0.1:8080']
labels:
service: 'myapp'
```
## Operations
### Service Management
```bash
# VictoriaMetrics
systemctl status victoriametrics
systemctl restart victoriametrics
journalctl -u victoriametrics -f
# Grafana
systemctl status grafana
systemctl restart grafana
journalctl -u grafana -f
# node_exporter
systemctl status node_exporter
systemctl restart node_exporter
journalctl -u node_exporter -f
```
### Data Locations
```
/var/lib/victoriametrics/ # Time-series data
/var/lib/grafana/ # Grafana database and dashboards
/var/log/grafana/ # Grafana logs
/etc/victoriametrics/ # VictoriaMetrics config
/etc/grafana/ # Grafana config
```
### Backup
VictoriaMetrics data is stored in `/var/lib/victoriametrics`:
```bash
# Stop service
systemctl stop victoriametrics
# Backup data
tar -czf victoriametrics-backup-$(date +%Y%m%d).tar.gz /var/lib/victoriametrics
# Start service
systemctl start victoriametrics
```
Grafana dashboards are stored in SQLite database at `/var/lib/grafana/grafana.db`:
```bash
# Backup Grafana
systemctl stop grafana
tar -czf grafana-backup-$(date +%Y%m%d).tar.gz /var/lib/grafana /etc/grafana
systemctl start grafana
```
## Security
### Authentication
- Grafana protected by Authentik OAuth/OIDC
- Local admin account available for emergency access
- All services bind to localhost only
### Network Security
- VictoriaMetrics: `127.0.0.1:8428` (no external access)
- Grafana: `127.0.0.1:3000` (proxied via Caddy with HTTPS)
- node_exporter: `127.0.0.1:9100` (no external access)
### systemd Hardening
All services run with security restrictions:
- `NoNewPrivileges=true`
- `ProtectSystem=strict`
- `ProtectHome=true`
- `PrivateTmp=true`
- Read-only filesystem (except data directories)
## Troubleshooting
### Grafana OAuth Not Working
1. Check Authentik provider configuration:
```bash
# Verify redirect URI matches
# https://metrics.jnss.me/login/generic_oauth
```
2. Check Grafana logs:
```bash
journalctl -u grafana -f
```
3. Verify OAuth credentials in vault match Authentik
### No Metrics in Grafana
1. Check VictoriaMetrics scrape targets:
```bash
curl http://127.0.0.1:8428/api/v1/targets
```
2. Check node_exporter is running:
```bash
systemctl status node_exporter
curl http://127.0.0.1:9100/metrics
```
3. Check VictoriaMetrics logs:
```bash
journalctl -u victoriametrics -f
```
### High Memory Usage
VictoriaMetrics is configured to use max 60% of available memory. Adjust if needed:
```yaml
# In roles/metrics/defaults/main.yml
victoriametrics_memory_allowed_percent: "40" # Reduce to 40%
```
## See Also
- [VictoriaMetrics Documentation](https://docs.victoriametrics.com/)
- [Grafana Documentation](https://grafana.com/docs/)
- [node_exporter GitHub](https://github.com/prometheus/node_exporter)
- [PromQL Documentation](https://prometheus.io/docs/prometheus/latest/querying/basics/)
- [Authentik OAuth Integration](https://goauthentik.io/docs/providers/oauth2/)

View File

@@ -0,0 +1,178 @@
---
# =================================================================
# Metrics Infrastructure Role - Complete Monitoring Stack
# =================================================================
# Provides VictoriaMetrics, Grafana, and node_exporter as unified stack
# =================================================================
# VictoriaMetrics Configuration
# =================================================================
# Service Management
victoriametrics_service_enabled: true
victoriametrics_service_state: "started"
# Version
victoriametrics_version: "1.105.0"
# Network Security (localhost only)
victoriametrics_listen_address: "127.0.0.1:8428"
# Storage Configuration
victoriametrics_data_dir: "/var/lib/victoriametrics"
victoriametrics_retention_period: "12" # months
# User/Group
victoriametrics_user: "victoriametrics"
victoriametrics_group: "victoriametrics"
# Performance Settings
victoriametrics_memory_allowed_percent: "30"
victoriametrics_storage_min_free_disk_space_bytes: "10GB"
# Scrape Configuration
victoriametrics_scrape_config_dir: "/etc/victoriametrics"
victoriametrics_scrape_config_file: "{{ victoriametrics_scrape_config_dir }}/scrape.yml"
victoriametrics_scrape_interval: "15s"
victoriametrics_scrape_timeout: "10s"
# systemd security
victoriametrics_systemd_security: true
# =================================================================
# Grafana Configuration
# =================================================================
# Service Management
grafana_service_enabled: true
grafana_service_state: "started"
# Version
grafana_version: "11.4.0"
# Network Security (localhost only - proxied via Caddy)
grafana_listen_address: "127.0.0.1"
grafana_listen_port: 3420
# User/Group
grafana_user: "grafana"
grafana_group: "grafana"
# Directories
grafana_data_dir: "/var/lib/grafana"
grafana_logs_dir: "/var/log/grafana"
grafana_plugins_dir: "/var/lib/grafana/plugins"
grafana_provisioning_dir: "/etc/grafana/provisioning"
# Domain Configuration
grafana_domain: "metrics.{{ caddy_domain }}"
grafana_root_url: "https://{{ grafana_domain }}"
# Default admin (used only for initial setup)
grafana_admin_user: "admin"
grafana_admin_password: "{{ vault_grafana_admin_password }}"
# Disable registration (OAuth only)
grafana_allow_signup: false
grafana_disable_login_form: false # Keep fallback login
# OAuth/OIDC Configuration (Authentik)
grafana_oauth_enabled: true
grafana_oauth_name: "Authentik"
grafana_oauth_client_id: "{{ vault_grafana_oauth_client_id }}"
grafana_oauth_client_secret: "{{ vault_grafana_oauth_client_secret }}"
# Authentik OAuth endpoints
grafana_oauth_auth_url: "https://{{ authentik_domain }}/application/o/authorize/"
grafana_oauth_token_url: "https://{{ authentik_domain }}/application/o/token/"
grafana_oauth_api_url: "https://{{ authentik_domain }}/application/o/userinfo/"
# OAuth role mapping
grafana_oauth_role_attribute_path: "(contains(groups, 'authentik Admins') || contains(groups, 'grafana-admins')) && 'Admin' || contains(groups, 'grafana-editors') && 'Editor' || 'Viewer'"
grafana_oauth_allow_sign_up: true # Auto-create users from OAuth
grafana_oauth_scopes: "openid profile email groups"
# Data Source Configuration
grafana_datasource_vm_enabled: true
grafana_datasource_vm_url: "http://{{ victoriametrics_listen_address }}"
grafana_datasource_vm_name: "VictoriaMetrics"
# Security
grafana_systemd_security: true
grafana_cookie_secure: true
grafana_cookie_samesite: "lax"
# Database (SQLite by default)
grafana_database_type: "sqlite3"
grafana_database_path: "{{ grafana_data_dir }}/grafana.db"
# =================================================================
# Node Exporter Configuration
# =================================================================
# Service Management
node_exporter_service_enabled: true
node_exporter_service_state: "started"
# Version
node_exporter_version: "1.8.2"
# Network Security (localhost only)
node_exporter_listen_address: "127.0.0.1:9100"
# User/Group
node_exporter_user: "node_exporter"
node_exporter_group: "node_exporter"
# Enabled collectors
node_exporter_enabled_collectors:
- cpu
- diskstats
- filesystem
- loadavg
- meminfo
- netdev
- netstat
- stat
- time
- uname
- vmstat
- systemd
# Disabled collectors
node_exporter_disabled_collectors:
- mdadm
# Filesystem collector configuration
node_exporter_filesystem_ignored_fs_types:
- tmpfs
- devtmpfs
- devfs
- iso9660
- overlay
- aufs
- squashfs
node_exporter_filesystem_ignored_mount_points:
- /var/lib/containers/storage/.*
- /run/.*
- /sys/.*
- /proc/.*
# systemd security
node_exporter_systemd_security: true
# =================================================================
# Infrastructure Notes
# =================================================================
# Complete monitoring stack:
# - VictoriaMetrics: Time-series database (Prometheus-compatible)
# - Grafana: Visualization with Authentik OAuth integration
# - node_exporter: System metrics collection
#
# Role mapping via Authentik groups:
# - grafana-admins: Full admin access
# - grafana-editors: Can create/edit dashboards
# - Default: Viewer access
#
# All services run on localhost only, proxied via Caddy

View File

@@ -0,0 +1,23 @@
---
- name: restart victoriametrics
ansible.builtin.systemd:
name: victoriametrics
state: restarted
daemon_reload: true
- name: restart node_exporter
ansible.builtin.systemd:
name: node_exporter
state: restarted
daemon_reload: true
- name: restart grafana
ansible.builtin.systemd:
name: grafana
state: restarted
daemon_reload: true
- name: reload caddy
ansible.builtin.systemd:
name: caddy
state: reloaded

View File

@@ -0,0 +1,3 @@
---
dependencies:
- role: caddy

View File

@@ -0,0 +1,9 @@
---
- name: Deploy Grafana Caddy configuration
ansible.builtin.template:
src: grafana.caddy.j2
dest: /etc/caddy/sites-enabled/grafana.caddy
owner: caddy
group: caddy
mode: '0644'
notify: reload caddy

View File

@@ -0,0 +1,90 @@
---
- name: Create Grafana system user
ansible.builtin.user:
name: "{{ grafana_user }}"
system: true
create_home: false
shell: /usr/sbin/nologin
state: present
- name: Create Grafana directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ grafana_user }}"
group: "{{ grafana_group }}"
mode: '0755'
loop:
- "{{ grafana_data_dir }}"
- "{{ grafana_logs_dir }}"
- "{{ grafana_plugins_dir }}"
- "{{ grafana_provisioning_dir }}"
- "{{ grafana_provisioning_dir }}/datasources"
- "{{ grafana_provisioning_dir }}/dashboards"
- "{{ grafana_data_dir }}/dashboards"
- /etc/grafana
- name: Download Grafana binary
ansible.builtin.get_url:
url: "https://dl.grafana.com/oss/release/grafana-{{ grafana_version }}.linux-amd64.tar.gz"
dest: "/tmp/grafana-{{ grafana_version }}.tar.gz"
mode: '0644'
register: grafana_download
- name: Extract Grafana
ansible.builtin.unarchive:
src: "/tmp/grafana-{{ grafana_version }}.tar.gz"
dest: /opt
remote_src: true
creates: "/opt/grafana-v{{ grafana_version }}"
when: grafana_download.changed
- name: Create Grafana symlink
ansible.builtin.file:
src: "/opt/grafana-v{{ grafana_version }}"
dest: /opt/grafana
state: link
- name: Deploy Grafana configuration
ansible.builtin.template:
src: grafana.ini.j2
dest: /etc/grafana/grafana.ini
owner: "{{ grafana_user }}"
group: "{{ grafana_group }}"
mode: '0640'
notify: restart grafana
- name: Deploy VictoriaMetrics datasource provisioning
ansible.builtin.template:
src: datasource-victoriametrics.yml.j2
dest: "{{ grafana_provisioning_dir }}/datasources/victoriametrics.yml"
owner: "{{ grafana_user }}"
group: "{{ grafana_group }}"
mode: '0644'
notify: restart grafana
when: grafana_datasource_vm_enabled
- name: Deploy dashboard provisioning
ansible.builtin.template:
src: dashboards.yml.j2
dest: "{{ grafana_provisioning_dir }}/dashboards/default.yml"
owner: "{{ grafana_user }}"
group: "{{ grafana_group }}"
mode: '0644'
notify: restart grafana
- name: Deploy Grafana systemd service
ansible.builtin.template:
src: grafana.service.j2
dest: /etc/systemd/system/grafana.service
owner: root
group: root
mode: '0644'
notify: restart grafana
- name: Enable and start Grafana service
ansible.builtin.systemd:
name: grafana
enabled: "{{ grafana_service_enabled }}"
state: "{{ grafana_service_state }}"
daemon_reload: true

View File

@@ -0,0 +1,20 @@
---
# =================================================================
# Metrics Stack Deployment
# =================================================================
- name: Deploy VictoriaMetrics
ansible.builtin.include_tasks: victoriametrics.yml
tags: [metrics, victoriametrics]
- name: Deploy node_exporter
ansible.builtin.include_tasks: node_exporter.yml
tags: [metrics, node_exporter]
- name: Deploy Grafana
ansible.builtin.include_tasks: grafana.yml
tags: [metrics, grafana]
- name: Deploy Caddy configuration for Grafana
ansible.builtin.include_tasks: caddy.yml
tags: [metrics, caddy]

View File

@@ -0,0 +1,49 @@
---
- name: Create node_exporter system user
ansible.builtin.user:
name: "{{ node_exporter_user }}"
system: true
create_home: false
shell: /usr/sbin/nologin
state: present
- name: Download node_exporter binary
ansible.builtin.get_url:
url: "https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/node_exporter-{{ node_exporter_version }}.linux-amd64.tar.gz"
dest: "/tmp/node_exporter-{{ node_exporter_version }}.tar.gz"
mode: '0644'
register: node_exporter_download
- name: Extract node_exporter binary
ansible.builtin.unarchive:
src: "/tmp/node_exporter-{{ node_exporter_version }}.tar.gz"
dest: /tmp
remote_src: true
creates: "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64"
when: node_exporter_download.changed
- name: Copy node_exporter binary to /usr/local/bin
ansible.builtin.copy:
src: "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64/node_exporter"
dest: /usr/local/bin/node_exporter
owner: root
group: root
mode: '0755'
remote_src: true
when: node_exporter_download.changed
- name: Deploy node_exporter systemd service
ansible.builtin.template:
src: node_exporter.service.j2
dest: /etc/systemd/system/node_exporter.service
owner: root
group: root
mode: '0644'
notify: restart node_exporter
- name: Enable and start node_exporter service
ansible.builtin.systemd:
name: node_exporter
enabled: "{{ node_exporter_service_enabled }}"
state: "{{ node_exporter_service_state }}"
daemon_reload: true

View File

@@ -0,0 +1,66 @@
---
- name: Create VictoriaMetrics system user
ansible.builtin.user:
name: "{{ victoriametrics_user }}"
system: true
create_home: false
shell: /usr/sbin/nologin
state: present
- name: Create VictoriaMetrics directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ victoriametrics_user }}"
group: "{{ victoriametrics_group }}"
mode: '0755'
loop:
- "{{ victoriametrics_data_dir }}"
- "{{ victoriametrics_scrape_config_dir }}"
- name: Download VictoriaMetrics binary
ansible.builtin.get_url:
url: "https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v{{ victoriametrics_version }}/victoria-metrics-linux-amd64-v{{ victoriametrics_version }}.tar.gz"
dest: "/tmp/victoria-metrics-v{{ victoriametrics_version }}.tar.gz"
mode: '0644'
register: victoriametrics_download
- name: Extract VictoriaMetrics binary
ansible.builtin.unarchive:
src: "/tmp/victoria-metrics-v{{ victoriametrics_version }}.tar.gz"
dest: /usr/local/bin
remote_src: true
creates: /usr/local/bin/victoria-metrics-prod
when: victoriametrics_download.changed
- name: Set VictoriaMetrics binary permissions
ansible.builtin.file:
path: /usr/local/bin/victoria-metrics-prod
owner: root
group: root
mode: '0755'
- name: Deploy VictoriaMetrics scrape configuration
ansible.builtin.template:
src: scrape.yml.j2
dest: "{{ victoriametrics_scrape_config_file }}"
owner: "{{ victoriametrics_user }}"
group: "{{ victoriametrics_group }}"
mode: '0644'
notify: restart victoriametrics
- name: Deploy VictoriaMetrics systemd service
ansible.builtin.template:
src: victoriametrics.service.j2
dest: /etc/systemd/system/victoriametrics.service
owner: root
group: root
mode: '0644'
notify: restart victoriametrics
- name: Enable and start VictoriaMetrics service
ansible.builtin.systemd:
name: victoriametrics
enabled: "{{ victoriametrics_service_enabled }}"
state: "{{ victoriametrics_service_state }}"
daemon_reload: true

View File

@@ -0,0 +1,12 @@
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: {{ grafana_data_dir }}/dashboards

View File

@@ -0,0 +1,12 @@
apiVersion: 1
datasources:
- name: {{ grafana_datasource_vm_name }}
type: prometheus
access: proxy
url: {{ grafana_datasource_vm_url }}
isDefault: true
editable: true
jsonData:
httpMethod: POST
timeInterval: 15s

View File

@@ -0,0 +1,26 @@
# Grafana Metrics Dashboard
{{ grafana_domain }} {
reverse_proxy http://{{ grafana_listen_address }}:{{ grafana_listen_port }} {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto https
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Host {host}
}
# Security headers
header {
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
# Logging
log {
output file {{ caddy_log_dir }}/grafana.log
level INFO
format json
}
}

View File

@@ -0,0 +1,68 @@
# Grafana Configuration
# Managed by Ansible - DO NOT EDIT MANUALLY
[paths]
data = {{ grafana_data_dir }}
logs = {{ grafana_logs_dir }}
plugins = {{ grafana_plugins_dir }}
provisioning = {{ grafana_provisioning_dir }}
[server]
http_addr = {{ grafana_listen_address }}
http_port = {{ grafana_listen_port }}
domain = {{ grafana_domain }}
root_url = {{ grafana_root_url }}
enforce_domain = true
enable_gzip = true
[database]
type = {{ grafana_database_type }}
{% if grafana_database_type == 'sqlite3' %}
path = {{ grafana_database_path }}
{% endif %}
[security]
admin_user = {{ grafana_admin_user }}
admin_password = {{ grafana_admin_password }}
secret_key = {{ vault_grafana_secret_key }}
cookie_secure = {{ grafana_cookie_secure | lower }}
cookie_samesite = {{ grafana_cookie_samesite }}
disable_gravatar = true
disable_initial_admin_creation = false
[users]
allow_sign_up = {{ grafana_allow_signup | lower }}
allow_org_create = false
auto_assign_org = true
auto_assign_org_role = Viewer
[auth]
disable_login_form = {{ grafana_disable_login_form | lower }}
oauth_auto_login = false
{% if grafana_oauth_enabled %}
[auth.generic_oauth]
enabled = true
name = {{ grafana_oauth_name }}
client_id = {{ grafana_oauth_client_id }}
client_secret = {{ grafana_oauth_client_secret }}
scopes = {{ grafana_oauth_scopes }}
auth_url = {{ grafana_oauth_auth_url }}
token_url = {{ grafana_oauth_token_url }}
api_url = {{ grafana_oauth_api_url }}
allow_sign_up = {{ grafana_oauth_allow_sign_up | lower }}
role_attribute_path = {{ grafana_oauth_role_attribute_path }}
use_pkce = true
{% endif %}
[log]
mode = console
level = info
[analytics]
reporting_enabled = false
check_for_updates = false
check_for_plugin_updates = false
[snapshots]
external_enabled = false

View File

@@ -0,0 +1,36 @@
[Unit]
Description=Grafana visualization platform
Documentation=https://grafana.com/docs/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ grafana_user }}
Group={{ grafana_group }}
WorkingDirectory=/opt/grafana
ExecStart=/opt/grafana/bin/grafana-server \
--config=/etc/grafana/grafana.ini \
--homepath=/opt/grafana
Restart=on-failure
RestartSec=5s
# Security hardening
{% if grafana_systemd_security %}
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths={{ grafana_data_dir }} {{ grafana_logs_dir }}
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
{% endif %}
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,42 @@
[Unit]
Description=Prometheus Node Exporter
Documentation=https://github.com/prometheus/node_exporter
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ node_exporter_user }}
Group={{ node_exporter_group }}
ExecStart=/usr/local/bin/node_exporter \
--web.listen-address={{ node_exporter_listen_address }} \
{% for collector in node_exporter_enabled_collectors %}
--collector.{{ collector }} \
{% endfor %}
{% for collector in node_exporter_disabled_collectors %}
--no-collector.{{ collector }} \
{% endfor %}
--collector.filesystem.fs-types-exclude="{{ node_exporter_filesystem_ignored_fs_types | join('|') }}" \
--collector.filesystem.mount-points-exclude="{{ node_exporter_filesystem_ignored_mount_points | join('|') }}"
Restart=on-failure
RestartSec=5s
# Security hardening
{% if node_exporter_systemd_security %}
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
ReadOnlyPaths=/
{% endif %}
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,22 @@
global:
scrape_interval: {{ victoriametrics_scrape_interval }}
scrape_timeout: {{ victoriametrics_scrape_timeout }}
external_labels:
environment: '{{ "homelab" if inventory_hostname in groups["homelab"] else "production" }}'
host: '{{ inventory_hostname }}'
scrape_configs:
# VictoriaMetrics self-monitoring
- job_name: 'victoriametrics'
static_configs:
- targets: ['{{ victoriametrics_listen_address }}']
labels:
service: 'victoriametrics'
# Node exporter for system metrics
- job_name: 'node'
static_configs:
- targets: ['{{ node_exporter_listen_address }}']
labels:
service: 'node_exporter'
instance: '{{ inventory_hostname }}'

View File

@@ -0,0 +1,41 @@
[Unit]
Description=VictoriaMetrics time-series database
Documentation=https://docs.victoriametrics.com/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ victoriametrics_user }}
Group={{ victoriametrics_group }}
ExecStart=/usr/local/bin/victoria-metrics-prod \
-storageDataPath={{ victoriametrics_data_dir }} \
-retentionPeriod={{ victoriametrics_retention_period }} \
-httpListenAddr={{ victoriametrics_listen_address }} \
-promscrape.config={{ victoriametrics_scrape_config_file }} \
-memory.allowedPercent={{ victoriametrics_memory_allowed_percent }} \
-storage.minFreeDiskSpaceBytes={{ victoriametrics_storage_min_free_disk_space_bytes }}
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
# Security hardening
{% if victoriametrics_systemd_security %}
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths={{ victoriametrics_data_dir }}
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
{% endif %}
[Install]
WantedBy=multi-user.target

View File

@@ -52,9 +52,17 @@ See `defaults/main.yml` for all configurable variables.
Define these in your `host_vars/` with `ansible-vault`: Define these in your `host_vars/` with `ansible-vault`:
```yaml ```yaml
# Core credentials (required)
vault_nextcloud_db_password: "secure-database-password" vault_nextcloud_db_password: "secure-database-password"
vault_nextcloud_admin_password: "secure-admin-password" vault_nextcloud_admin_password: "secure-admin-password"
vault_valkey_password: "secure-valkey-password" vault_valkey_password: "secure-valkey-password"
# Email credentials (optional - only if email enabled)
vault_nextcloud_smtp_password: "secure-smtp-password"
# OIDC credentials (optional - only if OIDC enabled)
vault_nextcloud_oidc_client_id: "nextcloud-client-id-from-authentik"
vault_nextcloud_oidc_client_secret: "nextcloud-client-secret-from-authentik"
``` ```
### Key Variables ### Key Variables
@@ -78,6 +86,462 @@ nextcloud_php_memory_limit: "512M"
nextcloud_php_upload_limit: "512M" nextcloud_php_upload_limit: "512M"
``` ```
## Deployment Strategy
This role uses a **two-phase deployment** approach to work correctly with the Nextcloud container's initialization process:
### Phase 1: Container Initialization (automatic)
1. Create empty directories for volumes
2. Deploy environment configuration (`.env`)
3. Start Nextcloud container
4. Container entrypoint detects first-time setup (no `version.php`)
5. Container copies Nextcloud files to `/var/www/html/`
6. Container runs `occ maintenance:install` with PostgreSQL
7. Installation creates `config.php` with database credentials
### Phase 2: Configuration via OCC Script (automatic)
8. Ansible waits for `occ status` to report `installed: true`
9. Ansible deploys and runs configuration script inside container
10. Script configures system settings via OCC commands:
- Redis caching (without sessions)
- Maintenance window and phone region
- Database optimizations (indices, bigint, mimetypes)
**Why this order?**
The Nextcloud container's entrypoint uses `version.php` as a marker to determine if installation is needed. We must wait for the container's auto-installation to complete before running configuration commands:
- Container must complete first-time setup (copy files, run `occ maintenance:install`)
- OCC commands require a fully initialized Nextcloud installation
- Running configuration after installation avoids conflicts with the entrypoint script
**Configuration Method:**
This role uses **OCC commands via a script** rather than config files because:
-**Explicit and verifiable** - Run `occ config:list system` to see exact state
-**No file conflicts** - Avoids issues with Docker image's built-in config files
-**Fully idempotent** - Safe to re-run during updates
-**Single source of truth** - All configuration in one script template
See the official [Nextcloud Docker documentation](https://github.com/nextcloud/docker#auto-configuration-via-environment-variables) for more details on the auto-configuration process.
## Installed Apps
This role automatically installs and enables the following apps:
- **user_oidc** - OpenID Connect authentication backend for SSO integration
- **calendar** - Calendar and scheduling application (CalDAV)
- **contacts** - Contact management application (CardDAV)
To customize the app list, override these variables in your `host_vars`:
```yaml
nextcloud_apps_install:
- user_oidc
- calendar
- contacts
- tasks # Add more apps as needed
- deck
- mail
```
## OIDC/SSO Integration
### Prerequisites
Before enabling OIDC, you must create an OIDC application/provider in your identity provider (e.g., Authentik):
**For Authentik:**
1. Navigate to **Applications → Providers**
2. Click **Create****OAuth2/OpenID Provider**
3. Configure:
- **Name**: `Nextcloud`
- **Authorization flow**: `default-authentication-flow` (or your preferred flow)
- **Client type**: `Confidential`
- **Client ID**: Generate or specify (save this)
- **Client Secret**: Generate or specify (save this)
- **Redirect URIs**: `https://cloud.jnss.me/apps/user_oidc/code`
- **Signing Key**: Select your signing certificate
- **Scopes**: Add `openid`, `profile`, `email`
4. Create **Application**:
- Navigate to **Applications → Applications**
- Click **Create**
- **Name**: `Nextcloud`
- **Slug**: `nextcloud`
- **Provider**: Select the provider created above
- **Launch URL**: `https://cloud.jnss.me`
5. Note the **Discovery URL**: `https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration`
### Configuration
Enable OIDC in your `host_vars/arch-vps/main.yml`:
```yaml
# OIDC Configuration
nextcloud_oidc_enabled: true
nextcloud_oidc_provider_id: "authentik" # Provider identifier (slug)
nextcloud_oidc_provider_name: "Authentik SSO" # Display name on login button
nextcloud_oidc_discovery_url: "https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration"
# Security settings (recommended defaults)
nextcloud_oidc_unique_uid: true # Prevents account takeover between providers
nextcloud_oidc_check_bearer: false
nextcloud_oidc_send_id_token_hint: true
# Attribute mappings (defaults work for most providers)
nextcloud_oidc_mapping_display_name: "name"
nextcloud_oidc_mapping_email: "email"
nextcloud_oidc_mapping_uid: "preferred_username" # Or "sub" for UUID
# Optional: Enable single login (auto-redirect to SSO)
nextcloud_oidc_single_login: false # Set to true to force SSO login
```
Add credentials to your vault file `host_vars/arch-vps/vault.yml`:
```yaml
vault_nextcloud_oidc_client_id: "nextcloud-client-id-from-authentik"
vault_nextcloud_oidc_client_secret: "nextcloud-client-secret-from-authentik"
```
### OIDC Scopes
The following scopes are requested from your OIDC provider by default:
```yaml
nextcloud_oidc_scope: "email profile nextcloud openid"
```
**Standard scopes:**
- `openid` - Required for OpenID Connect (contains no claims itself)
- `email` - User's email address (`email` and `email_verified` claims)
- `profile` - User's profile information (`name`, `given_name`, `preferred_username`, `picture`, etc.)
**Custom scope for Authentik:**
- `nextcloud` - Custom scope mapping you create in Authentik (contains `groups`, `quota`, `user_id`)
#### Creating the Nextcloud Scope Mapping in Authentik
The `nextcloud` scope must be created as a custom property mapping in Authentik:
1. Log in to Authentik as administrator
2. Navigate to **Customization****Property mappings****Create**
3. Select type: **Scope mapping**
4. Configure:
- **Name**: `Nextcloud Profile`
- **Scope name**: `nextcloud`
- **Expression**:
```python
# Extract all groups the user is a member of
groups = [group.name for group in user.ak_groups.all()]
# In Nextcloud, administrators must be members of a fixed group called "admin"
# If a user is an admin in authentik, ensure that "admin" is appended to their group list
if user.is_superuser and "admin" not in groups:
groups.append("admin")
return {
"name": request.user.name,
"groups": groups,
# Set a quota by using the "nextcloud_quota" property in the user's attributes
"quota": user.group_attributes().get("nextcloud_quota", None),
# To connect an existing Nextcloud user, set "nextcloud_user_id" to the Nextcloud username
"user_id": user.attributes.get("nextcloud_user_id", str(user.uuid)),
}
```
5. Click **Finish**
6. Navigate to your Nextcloud provider → **Advanced protocol settings**
7. Add `Nextcloud Profile` to **Scopes** (in addition to the default scopes)
### Group Provisioning and Synchronization
Automatically sync user group membership from Authentik to Nextcloud.
**Default configuration:**
```yaml
nextcloud_oidc_group_provisioning: true # Auto-create groups from Authentik
nextcloud_oidc_mapping_groups: "groups" # Claim containing group list
```
**How it works:**
1. User logs in via OIDC
2. Authentik sends group membership in the `groups` claim (from the custom scope)
3. Nextcloud automatically:
- Creates groups that don't exist in Nextcloud
- Adds user to those groups
- Removes user from groups they're no longer member of in Authentik
**Example: Making a user an admin**
Nextcloud requires admins to be in a group literally named `admin`. The custom scope mapping (above) automatically adds `"admin"` to the groups list for Authentik superusers.
Alternatively, manually create a group in Authentik called `admin` and add users to it.
**Quota management:**
Set storage quotas by adding the `nextcloud_quota` attribute to Authentik groups or users:
1. In Authentik, navigate to **Directory****Groups** → select your group
2. Under **Attributes**, add:
```json
{
"nextcloud_quota": "15 GB"
}
```
3. Users in this group will have a 15 GB quota in Nextcloud
4. If not set, quota is unlimited
### Complete Authentik Setup Guide
Follow these steps to set up OIDC authentication with Authentik:
**Step 1: Create the Custom Scope Mapping**
See [Creating the Nextcloud Scope Mapping in Authentik](#creating-the-nextcloud-scope-mapping-in-authentik) above.
**Step 2: Create the OAuth2/OpenID Provider**
1. In Authentik, navigate to **Applications** → **Providers**
2. Click **Create** → **OAuth2/OpenID Provider**
3. Configure:
- **Name**: `Nextcloud`
- **Authorization flow**: `default-authentication-flow` (or your preferred flow)
- **Client type**: `Confidential`
- **Client ID**: Generate or specify (save this for later)
- **Client Secret**: Generate or specify (save this for later)
- **Redirect URIs**: `https://cloud.jnss.me/apps/user_oidc/code`
- **Signing Key**: Select your signing certificate
- Under **Advanced protocol settings**:
- **Scopes**: Add `openid`, `email`, `profile`, and `Nextcloud Profile` (the custom scope created in Step 1)
- **Subject mode**: `Based on the User's UUID` (or `Based on the User's username` if you prefer usernames)
**Step 3: Create the Application**
1. Navigate to **Applications** → **Applications**
2. Click **Create**
3. Configure:
- **Name**: `Nextcloud`
- **Slug**: `nextcloud`
- **Provider**: Select the provider created in Step 2
- **Launch URL**: `https://cloud.jnss.me` (optional)
**Step 4: Note the Discovery URL**
The discovery URL follows this pattern:
```
https://auth.jnss.me/application/o/<slug>/.well-known/openid-configuration
```
For the application slug `nextcloud`, it will be:
```
https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration
```
**Step 5: Configure Nextcloud Role Variables**
In your `host_vars/arch-vps/main.yml`:
```yaml
nextcloud_oidc_enabled: true
nextcloud_oidc_provider_id: "authentik"
nextcloud_oidc_provider_name: "Authentik"
nextcloud_oidc_discovery_url: "https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration"
nextcloud_oidc_scope: "email profile nextcloud openid"
nextcloud_oidc_mapping_uid: "preferred_username" # Or "sub" for UUID-based IDs
nextcloud_oidc_mapping_display_name: "name"
nextcloud_oidc_mapping_email: "email"
nextcloud_oidc_mapping_groups: "groups"
nextcloud_oidc_mapping_quota: "quota"
nextcloud_oidc_group_provisioning: true
```
In your `host_vars/arch-vps/vault.yml`:
```yaml
vault_nextcloud_oidc_client_id: "nextcloud" # Client ID from Authentik
vault_nextcloud_oidc_client_secret: "very-long-secret-from-authentik" # Client Secret from Authentik
```
**Step 6: Deploy and Test**
Run the Nextcloud playbook:
```bash
ansible-playbook -i inventory/hosts.yml site.yml --tags nextcloud --ask-vault-pass
```
### Supported OIDC Providers
The `user_oidc` app supports any **OpenID Connect 1.0** compliant provider:
- **Authentik** (recommended for self-hosted)
- **Keycloak**
- **Auth0**
- **Okta**
- **Azure AD / Microsoft Entra ID**
- **Google Identity Platform**
- **GitHub** (via OIDC)
- **GitLab**
- **Authelia**
- **Kanidm**
- Any other OIDC 1.0 compliant provider
The `nextcloud_oidc_provider_id` is just an identifier slug - you can use any value like `authentik`, `keycloak`, `auth0`, `mycompany-sso`, etc.
### Verification
After deployment:
1. **Check provider configuration:**
```bash
podman exec --user www-data nextcloud php occ user_oidc:provider
podman exec --user www-data nextcloud php occ user_oidc:provider authentik
```
2. **Test login:**
- Visit `https://cloud.jnss.me`
- You should see a "Log in with Authentik SSO" button
- Click it to test SSO flow
- User account should be auto-created on first login
3. **Check user mapping:**
```bash
podman exec --user www-data nextcloud php occ user:list
```
### Troubleshooting OIDC
**Login button doesn't appear:**
```bash
# Check if user_oidc app is enabled
podman exec --user www-data nextcloud php occ app:list | grep user_oidc
# Enable if needed
podman exec --user www-data nextcloud php occ app:enable user_oidc
```
**Discovery URL errors:**
```bash
# Test discovery URL is accessible from container
podman exec nextcloud curl -k https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration
```
**JWKS cache issues:**
```bash
# Clear JWKS cache
podman exec --user www-data nextcloud php occ user_oidc:provider authentik \
--clientid='your-client-id'
```
## Email Configuration
Configure Nextcloud to send emails for password resets, notifications, and sharing.
### Configuration
Enable email in your `host_vars/arch-vps/main.yml`:
```yaml
# Email Configuration
nextcloud_email_enabled: true
nextcloud_smtp_host: "smtp.fastmail.com"
nextcloud_smtp_port: 587
nextcloud_smtp_secure: "tls" # tls, ssl, or empty
nextcloud_smtp_username: "nextcloud@jnss.me"
nextcloud_mail_from_address: "nextcloud"
nextcloud_mail_domain: "jnss.me"
# Set admin user's email address
nextcloud_admin_email: "admin@jnss.me"
```
Add SMTP password to vault `host_vars/arch-vps/vault.yml`:
```yaml
vault_nextcloud_smtp_password: "your-smtp-app-password"
```
### Common SMTP Providers
**Fastmail:**
```yaml
nextcloud_smtp_host: "smtp.fastmail.com"
nextcloud_smtp_port: 587
nextcloud_smtp_secure: "tls"
```
**Gmail (App Password required):**
```yaml
nextcloud_smtp_host: "smtp.gmail.com"
nextcloud_smtp_port: 587
nextcloud_smtp_secure: "tls"
```
**Office 365:**
```yaml
nextcloud_smtp_host: "smtp.office365.com"
nextcloud_smtp_port: 587
nextcloud_smtp_secure: "tls"
```
**SMTP2GO:**
```yaml
nextcloud_smtp_host: "mail.smtp2go.com"
nextcloud_smtp_port: 587
nextcloud_smtp_secure: "tls"
```
### Verification
After deployment:
1. **Check SMTP configuration:**
```bash
podman exec --user www-data nextcloud php occ config:list system | grep mail
```
2. **Check admin email:**
```bash
podman exec --user www-data nextcloud php occ user:setting admin settings email
```
3. **Send test email via Web UI:**
- Log in as admin
- Settings → Administration → Basic settings
- Scroll to "Email server"
- Click "Send email" button
- Check recipient inbox
### Troubleshooting Email
**Test SMTP connection from container:**
```bash
# Install swaks if needed (for testing)
podman exec nextcloud apk add --no-cache swaks
# Test SMTP connection
podman exec nextcloud swaks \
--to recipient@example.com \
--from nextcloud@jnss.me \
--server smtp.fastmail.com:587 \
--auth LOGIN \
--auth-user nextcloud@jnss.me \
--auth-password 'your-password' \
--tls
```
**Check Nextcloud logs:**
```bash
podman exec --user www-data nextcloud php occ log:watch
```
## Usage ## Usage
### Include in Playbook ### Include in Playbook
@@ -216,7 +680,7 @@ This role uses a **split caching strategy** for optimal performance and stabilit
- `memcache.local`: APCu (in-memory opcode cache) - `memcache.local`: APCu (in-memory opcode cache)
- `memcache.distributed`: Redis (shared cache, file locking) - `memcache.distributed`: Redis (shared cache, file locking)
- `memcache.locking`: Redis (transactional file locking) - `memcache.locking`: Redis (transactional file locking)
- Configuration: Via custom `redis.config.php` template - Configuration: Via OCC commands in configuration script
**Why not Redis sessions?** **Why not Redis sessions?**
@@ -228,15 +692,17 @@ The official Nextcloud Docker image enables Redis session handling when `REDIS_H
4. **Worker exhaustion**: Limited FPM workers (default 5) all become blocked 4. **Worker exhaustion**: Limited FPM workers (default 5) all become blocked
5. **Cascading failure**: New requests queue, timeouts accumulate, locks orphan 5. **Cascading failure**: New requests queue, timeouts accumulate, locks orphan
This role disables Redis sessions by **not setting** `REDIS_HOST` in the environment, while still providing Redis caching via a custom `redis.config.php` that is deployed independently. This role disables Redis sessions by **not setting** `REDIS_HOST` in the environment, while still providing Redis caching via OCC configuration commands.
**If you need Redis sessions** (e.g., multi-server setup with session sharing), you must: **If you need Redis sessions** (e.g., multi-server setup with session sharing), you must:
1. Enable `REDIS_HOST` in `nextcloud.env.j2` 1. Enable `REDIS_HOST` in `nextcloud.env.j2`
2. Set proper lock parameters in a custom PHP ini file 2. Add a custom PHP ini file with proper lock parameters:
3. Increase FPM workers significantly (15-20+) - `redis.session.lock_expire = 30` (locks expire after 30 seconds)
4. Monitor for orphaned session locks - `redis.session.lock_retries = 100` (max 100 retries, not infinite)
- `redis.session.lock_wait_time = 50000` (50ms between retries)
See `templates/redis-session-override.ini.j2` for an example of session lock tuning. 3. Mount the ini file with `zz-` prefix to load after the entrypoint's redis-session.ini
4. Increase FPM workers significantly (15-20+)
5. Monitor for orphaned session locks
## Troubleshooting ## Troubleshooting

View File

@@ -1,22 +1,104 @@
# Nextcloud Role - Required Vault Variables # Nextcloud Role - Vault Variables
This role requires the following encrypted variables to be defined in your vault file (typically `host_vars/<hostname>/vault.yml`). This document describes all vault-encrypted variables used by the Nextcloud role.
## Required Variables ## Required Variables
Add these to your encrypted vault file: These variables **must** be defined in your vault file for the role to function:
```yaml ```yaml
# Nextcloud database password # =================================================================
# Core Credentials (REQUIRED)
# =================================================================
# PostgreSQL database password for Nextcloud user
vault_nextcloud_db_password: "CHANGE_ME_secure_database_password" vault_nextcloud_db_password: "CHANGE_ME_secure_database_password"
# Nextcloud admin account password # Nextcloud admin user password
vault_nextcloud_admin_password: "CHANGE_ME_secure_admin_password" vault_nextcloud_admin_password: "CHANGE_ME_secure_admin_password"
# Valkey/Redis password (shared infrastructure) # Valkey (Redis) password for caching (shared infrastructure)
vault_valkey_password: "CHANGE_ME_secure_valkey_password" vault_valkey_password: "CHANGE_ME_secure_valkey_password"
``` ```
## Optional Variables
These variables are only required if you enable the corresponding features:
### Email/SMTP Configuration
Only required if `nextcloud_email_enabled: true`:
```yaml
# =================================================================
# Email/SMTP Credentials (OPTIONAL)
# =================================================================
# SMTP server password for sending emails
# Used with nextcloud_smtp_username for authentication
vault_nextcloud_smtp_password: "your-smtp-password-or-app-password"
```
**Example for Gmail:**
- Use an [App Password](https://support.google.com/accounts/answer/185833)
- Do NOT use your main Google account password
**Example for Fastmail:**
- Use an [App Password](https://www.fastmail.help/hc/en-us/articles/360058752854)
### OIDC/SSO Configuration
Only required if `nextcloud_oidc_enabled: true`:
```yaml
# =================================================================
# OIDC/SSO Credentials (OPTIONAL)
# =================================================================
# OAuth2/OIDC Client ID from your identity provider
vault_nextcloud_oidc_client_id: "nextcloud"
# OAuth2/OIDC Client Secret from your identity provider
# IMPORTANT: Keep this secret! Anyone with this can impersonate your app
vault_nextcloud_oidc_client_secret: "very-long-random-secret-from-authentik"
```
## Complete Vault File Example
Here's a complete example of a vault file with all possible variables:
```yaml
---
# =================================================================
# Example Vault File
# =================================================================
# File: host_vars/arch-vps/vault.yml
# Encrypted with: ansible-vault encrypt host_vars/arch-vps/vault.yml
# Caddy TLS
vault_caddy_tls_email: "admin@jnss.me"
vault_cloudflare_api_token: "your-cloudflare-token"
# Authentik
vault_authentik_db_password: "authentik-db-password"
vault_authentik_secret_key: "authentik-secret-key"
vault_authentik_admin_password: "authentik-admin-password"
# Valkey (shared infrastructure)
vault_valkey_password: "V4lk3y!P@ssw0rd#R3d1s"
# Nextcloud - Core (always required)
vault_nextcloud_db_password: "XkN8vQ2mP9wR5tY7uI0oP3sA6dF8gH1j"
vault_nextcloud_admin_password: "AdminP@ssw0rd!SecureAndL0ng"
# Nextcloud - Email (optional)
vault_nextcloud_smtp_password: "fastmail-app-password-xyz123"
# Nextcloud - OIDC (optional)
vault_nextcloud_oidc_client_id: "nextcloud"
vault_nextcloud_oidc_client_secret: "aksk_authentik_secret_very_long_random_string"
```
## Creating/Editing Vault File ## Creating/Editing Vault File
### First Time Setup ### First Time Setup
@@ -37,6 +119,13 @@ ansible-vault edit host_vars/arch-vps/vault.yml
# Add the Nextcloud variables, then save and exit # Add the Nextcloud variables, then save and exit
``` ```
### View Vault Contents
```bash
# View vault file contents
ansible-vault view host_vars/arch-vps/vault.yml
```
### Password Generation ### Password Generation
Generate secure passwords: Generate secure passwords:
@@ -49,39 +138,26 @@ openssl rand -base64 32
pwgen -s 32 1 pwgen -s 32 1
``` ```
## Example Vault File ## Running Playbooks with Vault
Your `host_vars/arch-vps/vault.yml` should include: ### Interactive Password Prompt
```yaml
---
# Caddy TLS
vault_caddy_tls_email: "admin@jnss.me"
vault_cloudflare_api_token: "your-cloudflare-token"
# Authentik
vault_authentik_db_password: "authentik-db-password"
vault_authentik_secret_key: "authentik-secret-key"
vault_authentik_admin_password: "authentik-admin-password"
# Nextcloud (ADD THESE)
vault_nextcloud_db_password: "generated-password-1"
vault_nextcloud_admin_password: "generated-password-2"
# Valkey (shared infrastructure)
vault_valkey_password: "valkey-password"
```
## Deployment
When deploying, you'll need to provide the vault password:
```bash ```bash
# Deploy with vault password prompt ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass
ansible-playbook -i inventory/hosts.yml site.yml --tags nextcloud --ask-vault-pass ```
# Or use a password file ### Using a Password File
ansible-playbook -i inventory/hosts.yml site.yml --tags nextcloud --vault-password-file ~/.vault_pass
```bash
# Create password file (DO NOT COMMIT THIS!)
echo 'your-vault-password' > .vault_pass
chmod 600 .vault_pass
# Add to .gitignore
echo '.vault_pass' >> .gitignore
# Run playbook
ansible-playbook -i inventory/hosts.yml site.yml --vault-password-file .vault_pass
``` ```
## Security Notes ## Security Notes
@@ -92,6 +168,29 @@ ansible-playbook -i inventory/hosts.yml site.yml --tags nextcloud --vault-passwo
- Store vault password securely (password manager, encrypted file, etc.) - Store vault password securely (password manager, encrypted file, etc.)
- Consider using `ansible-vault rekey` to change vault password periodically - Consider using `ansible-vault rekey` to change vault password periodically
## Troubleshooting
### "Vault password incorrect"
**Problem:** Wrong vault password entered
**Solution:** Verify you're using the correct vault password
### "vault_nextcloud_db_password is undefined"
**Problem:** Variable not defined in vault file or vault file not loaded
**Solution:**
1. Verify variable exists in vault file:
```bash
ansible-vault view host_vars/arch-vps/vault.yml | grep vault_nextcloud
```
2. Ensure you're using `--ask-vault-pass`:
```bash
ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass
```
## Verification ## Verification
Check that variables are properly encrypted: Check that variables are properly encrypted:
@@ -103,3 +202,8 @@ cat host_vars/arch-vps/vault.yml
# Decrypt and view (requires password) # Decrypt and view (requires password)
ansible-vault view host_vars/arch-vps/vault.yml ansible-vault view host_vars/arch-vps/vault.yml
``` ```
## Reference
- [Ansible Vault Documentation](https://docs.ansible.com/ansible/latest/user_guide/vault.html)
- [Best Practices for Variables and Vaults](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#variables-and-vaults)

View File

@@ -15,7 +15,6 @@ nextcloud_home: /opt/nextcloud
nextcloud_html_dir: "{{ nextcloud_home }}/html" nextcloud_html_dir: "{{ nextcloud_home }}/html"
nextcloud_data_dir: "{{ nextcloud_home }}/data" nextcloud_data_dir: "{{ nextcloud_home }}/data"
nextcloud_config_dir: "{{ nextcloud_home }}/config" nextcloud_config_dir: "{{ nextcloud_home }}/config"
nextcloud_custom_apps_dir: "{{ nextcloud_home }}/custom_apps"
# Container configuration (FPM variant) # Container configuration (FPM variant)
nextcloud_version: "stable-fpm" nextcloud_version: "stable-fpm"
@@ -52,6 +51,7 @@ nextcloud_domain: "cloud.jnss.me"
# Admin user (auto-configured on first run) # Admin user (auto-configured on first run)
nextcloud_admin_user: "admin" nextcloud_admin_user: "admin"
nextcloud_admin_email: "admin@jnss.me"
nextcloud_admin_password: "{{ vault_nextcloud_admin_password }}" nextcloud_admin_password: "{{ vault_nextcloud_admin_password }}"
# Trusted domains (space-separated) # Trusted domains (space-separated)
@@ -67,6 +67,87 @@ nextcloud_overwriteprotocol: "https"
nextcloud_php_memory_limit: "512M" nextcloud_php_memory_limit: "512M"
nextcloud_php_upload_limit: "512M" nextcloud_php_upload_limit: "512M"
# =================================================================
# Background Jobs Configuration
# =================================================================
nextcloud_background_jobs_mode: "cron" # Options: ajax, webcron, cron
nextcloud_cron_interval: "5min" # How often cron runs (systemd timer)
# =================================================================
# Nextcloud System Configuration
# =================================================================
nextcloud_maintenance_window_start: 4 # Start hour (UTC) for maintenance window
nextcloud_default_phone_region: "NO" # Default phone region code (ISO 3166-1 alpha-2)
# =================================================================
# Apps Configuration
# =================================================================
# Apps to install and enable
nextcloud_apps_install:
- user_oidc
- calendar
- contacts
# =================================================================
# Email/SMTP Configuration (Optional)
# =================================================================
nextcloud_email_enabled: true # Master switch - set to true to enable SMTP
# SMTP Server Configuration
nextcloud_smtp_mode: "smtp" # smtp, sendmail, qmail
nextcloud_smtp_host: "smtp.titan.email" # e.g., smtp.gmail.com, smtp.fastmail.com
nextcloud_smtp_port: 587 # 587 for TLS, 465 for SSL, 25 for plain
nextcloud_smtp_secure: "tls" # tls, ssl, or empty string for no encryption
nextcloud_smtp_auth: true # Enable SMTP authentication
nextcloud_smtp_authtype: "PLAIN" # LOGIN or PLAIN
nextcloud_smtp_username: "hello@jnss.me" # SMTP username
nextcloud_smtp_password: "{{ vault_smtp_password | default('') }}"
# Email Addressing
nextcloud_mail_from_address: "hello" # Local part only (before @)
nextcloud_mail_domain: "jnss.me" # Domain part (after @)
# Admin User Email (set at line 55 in Core Configuration section)
# =================================================================
# OIDC/SSO Configuration (Optional)
# =================================================================
nextcloud_oidc_enabled: true # Master switch - set to true to enable OIDC
# Provider Configuration
nextcloud_oidc_provider_id: "authentik" # Provider identifier (slug)
nextcloud_oidc_provider_name: "Authentik" # Display name (shown on login button)
nextcloud_oidc_client_id: "{{ vault_nextcloud_oidc_client_id | default('') }}"
nextcloud_oidc_client_secret: "{{ vault_nextcloud_oidc_client_secret | default('') }}"
nextcloud_oidc_discovery_url: "https://auth.jnss.me/application/o/nextcloud/.well-known/openid-configuration" # Full discovery URL, e.g., https://auth.example.com/application/o/nextcloud/.well-known/openid-configuration
# Scopes (based on Authentik integration guide)
# The 'nextcloud' scope is a custom scope you must create in Authentik
nextcloud_oidc_scope: "email profile nextcloud openid"
# Provider Options
nextcloud_oidc_unique_uid: false # Hash provider+user ID to prevent account takeover (recommended: true)
nextcloud_oidc_check_bearer: false # Check bearer tokens for API/WebDAV calls
nextcloud_oidc_send_id_token_hint: true # Send ID token hint during logout
# Attribute Mappings (based on Authentik integration guide)
nextcloud_oidc_mapping_display_name: "name" # Claim for display name
nextcloud_oidc_mapping_email: "email" # Claim for email
nextcloud_oidc_mapping_quota: "quota" # Claim for quota (from Authentik property mapping)
nextcloud_oidc_mapping_uid: "preferred_username" # Claim for user ID
nextcloud_oidc_mapping_groups: "groups" # Claim for groups (from Authentik property mapping)
# Group Provisioning (based on Authentik integration guide)
nextcloud_oidc_group_provisioning: true # Auto-create groups from OIDC provider
# Single Login Option
nextcloud_oidc_single_login: true # If true and only one provider, auto-redirect to SSO
# ================================================================= # =================================================================
# Caddy Integration # Caddy Integration
# ================================================================= # =================================================================

View File

@@ -0,0 +1,36 @@
---
# =================================================================
# Nextcloud Configuration via Script
# =================================================================
# Rick-Infra - Nextcloud Role
#
# Deploys and runs a configuration script inside the Nextcloud
# container to set system configuration via OCC commands.
- name: Deploy Nextcloud configuration script
template:
src: configure-nextcloud.sh.j2
dest: "{{ nextcloud_config_dir }}/configure.sh"
mode: '0755'
tags: [config, nextcloud-config]
- name: Run Nextcloud configuration script
command: podman exec --user www-data nextcloud bash /var/www/html/config/configure.sh
register: nc_config_result
changed_when: false # Script output doesn't indicate changes reliably
failed_when: nc_config_result.rc != 0
tags: [config, nextcloud-config]
- name: Display configuration script output
debug:
msg: "{{ nc_config_result.stdout_lines }}"
when: nc_config_result.stdout | length > 0
tags: [config, nextcloud-config]
- name: Display configuration script errors
debug:
msg: "{{ nc_config_result.stderr_lines }}"
when:
- nc_config_result.stderr | length > 0
- nc_config_result.rc != 0
tags: [config, nextcloud-config]

View File

@@ -0,0 +1,72 @@
---
# =================================================================
# Nextcloud Background Jobs Configuration
# =================================================================
# Rick-Infra - Nextcloud Role
#
# Configures systemd timer for reliable background job execution
# instead of Ajax-based cron (which requires user activity)
- name: Create nextcloud cron service
copy:
content: |
[Unit]
Description=Nextcloud Background Jobs (cron.php)
Documentation=https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/background_jobs_configuration.html
After=nextcloud.service
Requires=nextcloud.service
[Service]
Type=oneshot
ExecStart=/usr/bin/podman exec --user www-data nextcloud php -f /var/www/html/cron.php
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nextcloud-cron
dest: /etc/systemd/system/nextcloud-cron.service
mode: '0644'
backup: yes
notify: reload systemd
- name: Create nextcloud cron timer
copy:
content: |
[Unit]
Description=Nextcloud Background Jobs Timer
Documentation=https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/background_jobs_configuration.html
[Timer]
OnBootSec=5min
OnUnitActiveSec={{ nextcloud_cron_interval }}
Unit=nextcloud-cron.service
[Install]
WantedBy=timers.target
dest: /etc/systemd/system/nextcloud-cron.timer
mode: '0644'
backup: yes
notify: reload systemd
- name: Enable and start nextcloud cron timer
systemd:
name: nextcloud-cron.timer
enabled: yes
state: started
daemon_reload: yes
- name: Configure Nextcloud to use cron for background jobs
command: >
podman exec --user www-data nextcloud
php occ background:cron
register: nextcloud_cron_mode
changed_when: "'background jobs mode changed' in nextcloud_cron_mode.stdout or 'Set mode for background jobs to' in nextcloud_cron_mode.stdout"
failed_when:
- nextcloud_cron_mode.rc != 0
- "'mode for background jobs is currently' not in nextcloud_cron_mode.stdout"
- name: Verify cron timer is active
command: systemctl is-active nextcloud-cron.timer
register: timer_status
changed_when: false
failed_when: timer_status.stdout != "active"

View File

@@ -40,7 +40,6 @@
- "{{ nextcloud_html_dir }}" - "{{ nextcloud_html_dir }}"
- "{{ nextcloud_data_dir }}" - "{{ nextcloud_data_dir }}"
- "{{ nextcloud_config_dir }}" - "{{ nextcloud_config_dir }}"
- "{{ nextcloud_custom_apps_dir }}"
tags: [setup, directories] tags: [setup, directories]
- name: Deploy environment configuration - name: Deploy environment configuration
@@ -52,21 +51,9 @@
notify: restart nextcloud notify: restart nextcloud
tags: [config] tags: [config]
- name: Deploy custom Redis caching configuration # NOTE: Nextcloud is configured via OCC commands in a script after installation
template: # completes. This avoids interfering with the container's initialization process
src: redis.config.php.j2 # and provides a clean, explicit configuration approach.
dest: "{{ nextcloud_config_dir }}/redis.config.php"
mode: '0640'
notify: restart nextcloud
tags: [config, redis]
- name: Deploy Redis session lock override configuration
template:
src: redis-session-override.ini.j2
dest: "{{ nextcloud_home }}/redis-session-override.ini"
mode: '0644'
notify: restart nextcloud
tags: [config, redis]
- name: Create Quadlet systemd directory (system scope) - name: Create Quadlet systemd directory (system scope)
file: file:
@@ -91,7 +78,6 @@
owner: root owner: root
group: "{{ caddy_user }}" group: "{{ caddy_user }}"
mode: '0644' mode: '0644'
backup: true
notify: reload caddy notify: reload caddy
tags: [caddy, reverse-proxy] tags: [caddy, reverse-proxy]
@@ -130,21 +116,51 @@
delay: 10 delay: 10
tags: [verification] tags: [verification]
- name: Wait for Nextcloud installation to complete
shell: podman exec nextcloud php occ status --output=json 2>/dev/null || echo '{"installed":false}'
register: nc_status
until: (nc_status.stdout | from_json).installed | default(false) == true
retries: 60
delay: 5
changed_when: false
tags: [verification]
- name: Configure Nextcloud via OCC script
include_tasks: configure.yml
tags: [config, configure]
- name: Truncate nextcloud.log to prevent bloat
shell: |
podman exec nextcloud truncate -s 0 /var/www/html/data/nextcloud.log || true
changed_when: false
failed_when: false
tags: [maintenance, cleanup]
- name: Configure background jobs (cron)
include_tasks: cron.yml
tags: [cron, background-jobs]
- name: Display Nextcloud deployment status - name: Display Nextcloud deployment status
debug: debug:
msg: | msg: |
✅ Nextcloud Cloud Storage deployed successfully! ✅ Nextcloud Cloud Storage deployed successfully!
🌐 Domain: {{ nextcloud_domain }} 🌐 Domain: {{ nextcloud_domain }}
🗄️ Database: {{ nextcloud_db_name }} (Unix socket) 🗄️ Database: {{ nextcloud_db_name }} (PostgreSQL via Unix socket)
🗄️ Cache: Valkey DB {{ nextcloud_valkey_db }} (Unix socket) 🗄️ Cache: Valkey DB {{ nextcloud_valkey_db }} (Redis-compatible via Unix socket)
🐳 Container: FPM via Podman Quadlet 🐳 Container: FPM via Podman Quadlet
🔒 Admin: {{ nextcloud_admin_user }} 🔒 Admin: {{ nextcloud_admin_user }}
⚙️ Configuration:
- Redis caching enabled (application-level cache & file locking)
- PHP sessions use file-based storage (not Redis)
- Database optimizations applied
- Configuration via OCC commands
🚀 Ready for file storage and collaboration! 🚀 Ready for file storage and collaboration!
📋 Next Steps: 📋 Next Steps:
- Access https://{{ nextcloud_domain }} to complete setup - Access https://{{ nextcloud_domain }} to log in
- Install desired Nextcloud apps - Install desired Nextcloud apps
- Configure user accounts - Configure user accounts and storage quotas
tags: [verification] tags: [verification]

View File

@@ -0,0 +1,189 @@
#!/bin/bash
# =================================================================
# Nextcloud Configuration Script
# =================================================================
# Rick-Infra - Nextcloud Role
#
# This script configures Nextcloud via OCC commands after initial
# installation. It is generated from Ansible variables and runs
# inside the Nextcloud container.
#
# Generated by: roles/nextcloud/templates/configure-nextcloud.sh.j2
# Managed by: Ansible
set +e # Continue on errors, report at end
ERRORS=0
# Helper function for OCC
occ() {
php /var/www/html/occ "$@" 2>&1
}
# Track errors
check_error() {
if [ $? -ne 0 ]; then
ERRORS=$((ERRORS + 1))
echo "ERROR: $1" >&2
fi
}
# =================================================================
# Redis Caching Configuration
# =================================================================
# Configure Redis for application-level caching and file locking
# WITHOUT enabling Redis sessions (which can cause performance issues)
occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' --quiet
check_error "Failed to set memcache.distributed"
occ config:system:set memcache.locking --value='\OC\Memcache\Redis' --quiet
check_error "Failed to set memcache.locking"
occ config:system:set redis host --value='{{ valkey_unix_socket_path }}' --quiet
check_error "Failed to set redis.host"
occ config:system:set redis password --value='{{ valkey_password }}' --quiet
check_error "Failed to set redis.password"
occ config:system:set redis dbindex --value={{ nextcloud_valkey_db }} --type=integer --quiet
check_error "Failed to set redis.dbindex"
# =================================================================
# Maintenance Configuration
# =================================================================
occ config:system:set maintenance_window_start --value={{ nextcloud_maintenance_window_start }} --type=integer --quiet
check_error "Failed to set maintenance_window_start"
occ config:system:set default_phone_region --value='{{ nextcloud_default_phone_region }}' --quiet
check_error "Failed to set default_phone_region"
# =================================================================
# Database Optimization
# =================================================================
# Add missing database indices
occ db:add-missing-indices --quiet
check_error "Failed to add missing database indices"
# Convert filecache to bigint
occ db:convert-filecache-bigint --no-interaction --quiet
check_error "Failed to convert filecache to bigint"
# Update mimetype database mappings
occ maintenance:repair --include-expensive --quiet
check_error "Failed to run maintenance:repair"
# =================================================================
# App Installation and Enablement
# =================================================================
# Install apps first, then enable them. This must happen before
# app-specific configuration (e.g., OIDC provider setup)
{% if nextcloud_apps_install is defined and nextcloud_apps_install | length > 0 %}
# Install apps
{% for app in nextcloud_apps_install %}
occ app:install {{ app }} --quiet 2>&1 | grep -v "already installed" || true
check_error "Failed to install app: {{ app }}"
{% endfor %}
{% endif %}
# =================================================================
# Email/SMTP Configuration
# =================================================================
{% if nextcloud_email_enabled | default(false) %}
# Configure SMTP mode
occ config:system:set mail_smtpmode --value={{ nextcloud_smtp_mode }} --quiet
check_error "Failed to set mail_smtpmode"
# Configure SMTP server
occ config:system:set mail_smtphost --value='{{ nextcloud_smtp_host }}' --quiet
check_error "Failed to set mail_smtphost"
occ config:system:set mail_smtpport --value={{ nextcloud_smtp_port }} --type=integer --quiet
check_error "Failed to set mail_smtpport"
{% if nextcloud_smtp_secure %}
occ config:system:set mail_smtpsecure --value={{ nextcloud_smtp_secure }} --quiet
check_error "Failed to set mail_smtpsecure"
{% endif %}
{% if nextcloud_smtp_auth %}
# Configure SMTP authentication
occ config:system:set mail_smtpauth --value=1 --type=integer --quiet
check_error "Failed to set mail_smtpauth"
occ config:system:set mail_smtpauthtype --value={{ nextcloud_smtp_authtype }} --quiet
check_error "Failed to set mail_smtpauthtype"
occ config:system:set mail_smtpname --value='{{ nextcloud_smtp_username }}' --quiet
check_error "Failed to set mail_smtpname"
occ config:system:set mail_smtppassword --value='{{ nextcloud_smtp_password }}' --quiet
check_error "Failed to set mail_smtppassword"
{% endif %}
# Configure email addressing
occ config:system:set mail_from_address --value='{{ nextcloud_mail_from_address }}' --quiet
check_error "Failed to set mail_from_address"
occ config:system:set mail_domain --value='{{ nextcloud_mail_domain }}' --quiet
check_error "Failed to set mail_domain"
{% endif %}
# Set admin user email address
{% if nextcloud_admin_email %}
occ user:setting {{ nextcloud_admin_user }} settings email '{{ nextcloud_admin_email }}' --quiet
check_error "Failed to set admin user email"
{% endif %}
# =================================================================
# OIDC/SSO Provider Configuration
# =================================================================
{% if nextcloud_oidc_enabled | default(false) %}
# Configure OIDC provider (creates if doesn't exist, updates if exists)
occ user_oidc:provider {{ nextcloud_oidc_provider_id }} \
--clientid='{{ nextcloud_oidc_client_id }}' \
--clientsecret='{{ nextcloud_oidc_client_secret }}' \
--discoveryuri='{{ nextcloud_oidc_discovery_url }}' \
--scope='{{ nextcloud_oidc_scope }}' \
--unique-uid={{ '1' if nextcloud_oidc_unique_uid else '0' }} \
--check-bearer={{ '1' if nextcloud_oidc_check_bearer else '0' }} \
--send-id-token-hint={{ '1' if nextcloud_oidc_send_id_token_hint else '0' }} \
{% if nextcloud_oidc_mapping_display_name %}
--mapping-display-name='{{ nextcloud_oidc_mapping_display_name }}' \
{% endif %}
{% if nextcloud_oidc_mapping_email %}
--mapping-email='{{ nextcloud_oidc_mapping_email }}' \
{% endif %}
{% if nextcloud_oidc_mapping_quota %}
--mapping-quota='{{ nextcloud_oidc_mapping_quota }}' \
{% endif %}
{% if nextcloud_oidc_mapping_uid %}
--mapping-uid='{{ nextcloud_oidc_mapping_uid }}' \
{% endif %}
{% if nextcloud_oidc_mapping_groups %}
--mapping-groups='{{ nextcloud_oidc_mapping_groups }}' \
{% endif %}
--group-provisioning={{ '1' if nextcloud_oidc_group_provisioning else '0' }} \
--quiet 2>&1 | grep -v "already exists" || true
check_error "Failed to configure OIDC provider: {{ nextcloud_oidc_provider_id }}"
{% if nextcloud_oidc_single_login %}
# Enable single login (auto-redirect to SSO if only one provider)
occ config:app:set user_oidc allow_multiple_user_backends --value=0 --quiet
check_error "Failed to enable single login mode"
{% endif %}
{% endif %}
# =================================================================
# Exit Status
# =================================================================
if [ $ERRORS -gt 0 ]; then
echo "Configuration completed with $ERRORS error(s)" >&2
exit 1
else
echo "Nextcloud configuration completed successfully"
exit 0
fi

View File

@@ -58,6 +58,10 @@
Referrer-Policy "no-referrer" Referrer-Policy "no-referrer"
# Disable FLoC tracking # Disable FLoC tracking
Permissions-Policy "interest-cohort=()" Permissions-Policy "interest-cohort=()"
# Robot indexing policy
X-Robots-Tag "noindex, nofollow"
# Cross-domain policy
X-Permitted-Cross-Domain-Policies "none"
# Remove server header # Remove server header
-Server -Server
} }

View File

@@ -23,12 +23,6 @@ Volume={{ nextcloud_data_dir }}:/var/www/html/data:Z
# Configuration (private - contains secrets) # Configuration (private - contains secrets)
Volume={{ nextcloud_config_dir }}:/var/www/html/config:Z Volume={{ nextcloud_config_dir }}:/var/www/html/config:Z
# Custom apps (world-readable)
Volume={{ nextcloud_custom_apps_dir }}:/var/www/html/custom_apps:Z
# Redis session configuration override (zz- prefix ensures it loads last)
Volume={{ nextcloud_home }}/redis-session-override.ini:/usr/local/etc/php/conf.d/zz-redis-session-override.ini:Z,ro
# Infrastructure sockets (mounted with world-readable permissions on host) # Infrastructure sockets (mounted with world-readable permissions on host)
Volume={{ postgresql_unix_socket_directories }}:{{ postgresql_unix_socket_directories }}:Z Volume={{ postgresql_unix_socket_directories }}:{{ postgresql_unix_socket_directories }}:Z
Volume={{ valkey_unix_socket_path | dirname }}:{{ valkey_unix_socket_path | dirname }}:Z Volume={{ valkey_unix_socket_path | dirname }}:{{ valkey_unix_socket_path | dirname }}:Z

View File

@@ -1,16 +0,0 @@
; Redis Session Lock Override for Nextcloud
; Prevents orphaned session locks from causing infinite hangs
;
; Default Nextcloud container settings:
; redis.session.lock_expire = 0 (locks NEVER expire - causes infinite hangs)
; redis.session.lock_retries = -1 (infinite retries - causes worker exhaustion)
; redis.session.lock_wait_time = 10000 (10 seconds per retry - very slow)
;
; These settings ensure locks auto-expire and failed requests don't block workers forever:
; - Locks expire after 30 seconds (prevents orphaned locks)
; - Max 100 retries = 5 seconds total wait time (prevents infinite loops)
; - 50ms wait between retries (reasonable balance)
redis.session.lock_expire = 30
redis.session.lock_retries = 100
redis.session.lock_wait_time = 50000

View File

@@ -1,34 +0,0 @@
<?php
/**
* Redis/Valkey Caching Configuration for Nextcloud
*
* This file provides Redis caching for Nextcloud application-level operations
* (distributed cache, file locking) WITHOUT enabling Redis for PHP sessions.
*
* IMPORTANT: This overrides the default /usr/src/nextcloud/config/redis.config.php
* which checks for REDIS_HOST environment variable. We deploy this custom version
* to enable Redis caching while keeping PHP sessions file-based for stability.
*
* Why not use REDIS_HOST env var?
* - Setting REDIS_HOST enables BOTH Redis sessions AND Redis caching
* - Redis session handling can cause severe performance issues:
* * Session lock contention under high concurrency
* * Infinite lock retries blocking FPM workers
* * Timeout orphaning leaving locks unreleased
* * Worker pool exhaustion causing cascading failures
*
* This configuration provides the benefits of Redis caching (fast distributed
* cache, reliable file locking) while avoiding the pitfalls of Redis sessions.
*
* Managed by: Ansible Nextcloud role
* Template: roles/nextcloud/templates/redis.config.php.j2
*/
$CONFIG = array(
'memcache.distributed' => '\OC\Memcache\Redis',
'memcache.locking' => '\OC\Memcache\Redis',
'redis' => array(
'host' => '{{ valkey_unix_socket_path }}',
'password' => '{{ valkey_password }}',
),
);

View File

@@ -34,6 +34,27 @@ Podman is deployed as a system-level infrastructure service that provides contai
All registries configured with HTTPS-only, no insecure connections allowed. All registries configured with HTTPS-only, no insecure connections allowed.
### **Private Registry Authentication:**
For private container images (e.g., from GitHub Container Registry), this role deploys authentication credentials:
- **Auth file**: `/etc/containers/auth.json` (system-wide, for root containers)
- **Permissions**: 0600 (root:root only)
- **Credentials**: Stored encrypted in Ansible Vault
- **Automatic**: Quadlet containers automatically use authentication when pulling images
**Configuration:**
```yaml
# In group_vars/production/vault.yml (encrypted)
vault_github_username: "your-username"
vault_github_token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# In group_vars/production/main.yml
github_username: "{{ vault_github_username }}"
github_token: "{{ vault_github_token }}"
```
When these variables are defined, the role automatically deploys authentication for ghcr.io.
## Application Integration ## Application Integration
Applications should create service-specific users and manage their own container deployments: Applications should create service-specific users and manage their own container deployments:

View File

@@ -46,6 +46,10 @@ podman_registry_blocked: false
podman_default_network: "bridge" podman_default_network: "bridge"
podman_network_security: true podman_network_security: true
# Trusted container subnets (allowed through firewall)
podman_trusted_subnets:
- "10.88.0.0/16"
# ================================================================= # =================================================================
# Storage Configuration # Storage Configuration
# ================================================================= # =================================================================

View File

@@ -11,3 +11,8 @@
name: podman name: podman
state: restarted state: restarted
when: podman_service_enabled | default(true) when: podman_service_enabled | default(true)
- name: reload nftables
systemd:
name: nftables
state: reloaded

View File

@@ -42,6 +42,22 @@
backup: yes backup: yes
notify: restart podman notify: restart podman
- name: Create default podman network with DNS enabled
command: podman network create podman --subnet 10.88.0.0/16
register: podman_network_create
changed_when: "'podman' in podman_network_create.stdout"
failed_when:
- podman_network_create.rc != 0
- "'already exists' not in podman_network_create.stderr"
- name: Deploy podman firewall rules
template:
src: podman.nft.j2
dest: /etc/nftables.d/10-podman.nft
mode: '0644'
backup: yes
notify: reload nftables
- name: Enable podman system service (if enabled) - name: Enable podman system service (if enabled)
systemd: systemd:
name: podman name: podman
@@ -61,6 +77,29 @@
changed_when: false changed_when: false
failed_when: false failed_when: false
# =================================================================
# Container Registry Authentication
# =================================================================
# Deploy system-wide authentication for private container registries
# Currently supports: GitHub Container Registry (ghcr.io)
- name: Deploy GitHub Container Registry authentication
copy:
content: |
{
"auths": {
"ghcr.io": {
"auth": "{{ (github_username + ':' + github_token) | b64encode }}"
}
}
}
dest: /etc/containers/auth.json
mode: '0600'
owner: root
group: root
when: github_username is defined and github_token is defined
no_log: true # Don't log sensitive authentication data
- name: Display Podman infrastructure status - name: Display Podman infrastructure status
debug: debug:
msg: | msg: |
@@ -70,6 +109,7 @@
🔒 Security: Rootless container runtime enabled 🔒 Security: Rootless container runtime enabled
📦 Registries: {{ podman_registries | join(', ') }} 📦 Registries: {{ podman_registries | join(', ') }}
🏗️ Storage: {{ 'overlay' if 'overlay' in podman_system_info.stdout else 'system default' }} 🏗️ Storage: {{ 'overlay' if 'overlay' in podman_system_info.stdout else 'system default' }}
🔑 Auth: {{ 'GitHub Container Registry configured' if (github_username is defined and github_token is defined) else 'No private registry auth' }}
🚀 Ready for containerized applications! 🚀 Ready for containerized applications!

View File

@@ -19,6 +19,9 @@ network_backend = "netavark"
# Default network for new containers # Default network for new containers
default_network = "{{ podman_default_network }}" default_network = "{{ podman_default_network }}"
# For signing into ghcr.io
auth_file = "/etc/containers/auth.json"
# ================================================================= # =================================================================
# Storage Configuration # Storage Configuration
# ================================================================= # =================================================================

View File

@@ -0,0 +1,32 @@
#!/usr/sbin/nft -f
# =================================================================
# Podman Container Network Firewall Rules
# =================================================================
# Rick-Infra Infrastructure - Podman Role
# Priority: 10 (loaded after base rules, before drop rules)
#
# Purpose:
# - Allow container-to-host communication for services (PostgreSQL, Valkey)
# - Allow container outbound traffic for DNS, package updates, etc.
# - Enable NAT/masquerading for container networks
#
# Security Model:
# - Containers are trusted (they run our own services)
# - All container egress traffic is allowed (simplified management)
# - Container ingress is controlled by application-specific port publishing
#
# Architecture:
# - Containers access host services via Unix sockets or host.containers.internal
# - Caddy reverse proxy handles all external traffic
# - No direct container port exposure to internet
# Add rules to INPUT chain - Allow trusted container subnets
{% for subnet in podman_trusted_subnets %}
add rule inet filter input ip saddr {{ subnet }} accept comment "Podman containers: {{ subnet }}"
{% endfor %}
# Add rules to FORWARD chain - Enable container forwarding
add rule inet filter forward ct state established,related accept comment "Allow established connections"
add rule inet filter forward iifname "podman0" accept comment "Allow outbound from podman bridge"
add rule inet filter forward oifname "podman0" ct state established,related accept comment "Allow inbound to podman bridge (established)"

View File

@@ -126,6 +126,11 @@
delay: 3 delay: 3
when: postgresql_service_state == "started" and postgresql_unix_socket_enabled and postgresql_listen_addresses == "" when: postgresql_service_state == "started" and postgresql_unix_socket_enabled and postgresql_listen_addresses == ""
# Containerized applications mount socket directories. If handlers run at the end of playbooks this will mount stale sockets.
- name: Flush handlers for socket connection
meta: flush_handlers
tags: always
- name: Display PostgreSQL infrastructure status - name: Display PostgreSQL infrastructure status
debug: debug:
msg: | msg: |

View File

@@ -25,7 +25,7 @@ sigvild_gallery_guest_username: guest
sigvild_gallery_guest_password: "{{ vault_sigvild_guest_password }}" sigvild_gallery_guest_password: "{{ vault_sigvild_guest_password }}"
# Build configuration # Build configuration
sigvild_gallery_local_project_path: "{{ ansible_env.PWD }}/sigvild-gallery" sigvild_gallery_local_project_path: "{{ lookup('env', 'HOME') }}/sigvild-gallery"
# Service configuration # Service configuration
sigvild_gallery_service_enabled: true sigvild_gallery_service_enabled: true
@@ -33,7 +33,7 @@ sigvild_gallery_service_state: started
# Backup configuration # Backup configuration
sigvild_gallery_backup_enabled: true sigvild_gallery_backup_enabled: true
sigvild_gallery_backup_local_path: "{{ playbook_dir }}/backups/sigvild-gallery" sigvild_gallery_backup_local_path: "{{ lookup('env', 'HOME') }}/sigvild-gallery-backup/"
# Caddy integration (assumes caddy role provides these) # Caddy integration (assumes caddy role provides these)
# caddy_sites_enabled_dir: /etc/caddy/sites-enabled # caddy_sites_enabled_dir: /etc/caddy/sites-enabled

View File

@@ -33,11 +33,7 @@
notify: restart sigvild-gallery notify: restart sigvild-gallery
tags: [backend] tags: [backend]
- name: Restore data from backup if available - name: Create data directory for PocketBase (if not created by restore)
include_tasks: restore.yml
tags: [backend, restore]
- name: Create data directory for PocketBase
file: file:
path: "{{ sigvild_gallery_data_dir }}" path: "{{ sigvild_gallery_data_dir }}"
state: directory state: directory

View File

@@ -14,7 +14,7 @@
home: "{{ sigvild_gallery_home }}" home: "{{ sigvild_gallery_home }}"
create_home: yes create_home: yes
- name: Create directories - name: Create directories (excluding pb_data, created later)
file: file:
path: "{{ item }}" path: "{{ item }}"
state: directory state: directory
@@ -23,7 +23,6 @@
mode: '0755' mode: '0755'
loop: loop:
- "{{ sigvild_gallery_home }}" - "{{ sigvild_gallery_home }}"
- "{{ sigvild_gallery_data_dir }}"
- "{{ sigvild_gallery_web_root }}" - "{{ sigvild_gallery_web_root }}"
- name: Check for existing gallery data - name: Check for existing gallery data

View File

@@ -143,6 +143,11 @@
failed_when: valkey_ping_result_socket.stdout != "PONG" failed_when: valkey_ping_result_socket.stdout != "PONG"
when: valkey_service_state == "started" and valkey_unix_socket_enabled when: valkey_service_state == "started" and valkey_unix_socket_enabled
# Containerized applications mount socket directories. If handlers run at the end of playbooks this will mount stale sockets.
- name: Flush handlers for socket connection
meta: flush_handlers
tags: always
- name: Display Valkey infrastructure status - name: Display Valkey infrastructure status
debug: debug:
msg: | msg: |

331
roles/vaultwarden/README.md Normal file
View File

@@ -0,0 +1,331 @@
# Vaultwarden Password Manager Role
Self-contained Vaultwarden (Bitwarden-compatible) password manager deployment using Podman and PostgreSQL.
## Overview
This role deploys Vaultwarden as a Podman Quadlet container with:
- **PostgreSQL backend** via Unix socket (777 permissions)
- **Caddy reverse proxy** with HTTPS and WebSocket support
- **SSO integration** ready (Authentik OpenID Connect)
- **SMTP support** for email notifications (optional)
- **Admin panel** for management
## Architecture
```
Internet → Caddy (HTTPS) → Vaultwarden Container → PostgreSQL (Unix socket)
/data volume
```
### Components
- **Container Image**: `vaultwarden/server:latest` (Docker Hub)
- **User**: System user `vaultwarden` (non-root)
- **Port**: 8080 (localhost only)
- **Domain**: `vault.jnss.me`
- **Database**: PostgreSQL via Unix socket at `/var/run/postgresql`
- **Data**: `/opt/vaultwarden/data`
## Dependencies
**Managed Hosts:**
- `postgresql` role (provides database and Unix socket)
- `caddy` role (provides reverse proxy)
**Control Node:**
- `argon2` command-line tool (automatically installed if not present)
- Used to hash the admin token securely on the control node
- Available in most package managers: `pacman -S argon2`, `apt install argon2`, etc.
## Configuration
### Required Variables
Must be defined in vault (e.g., `group_vars/homelab/vault.yml`):
```yaml
# Database password
vault_vaultwarden_db_password: "secure-database-password"
# Admin token (plain text - will be hashed automatically during deployment)
vault_vaultwarden_admin_token: "your-secure-admin-token"
# SMTP password (if using email)
vault_vaultwarden_smtp_password: "smtp-password" # optional
# SSO credentials (if using Authentik integration)
vault_vaultwarden_sso_client_id: "vaultwarden" # optional
vault_vaultwarden_sso_client_secret: "sso-secret" # optional
```
### Optional Variables
Override in `group_vars` or `host_vars`:
```yaml
# Domain
vaultwarden_domain: "vault.jnss.me"
# Container version
vaultwarden_version: "latest"
# Registration controls
vaultwarden_signups_allowed: false # Disable open registration
vaultwarden_invitations_allowed: true # Allow existing users to invite
# SMTP Configuration
vaultwarden_smtp_enabled: true
vaultwarden_smtp_host: "smtp.example.com"
vaultwarden_smtp_port: 587
vaultwarden_smtp_from: "vault@jnss.me"
vaultwarden_smtp_username: "smtp-user"
# SSO Configuration (Authentik)
vaultwarden_sso_enabled: true
vaultwarden_sso_authority: "https://auth.jnss.me"
```
## Usage
### Deploy Vaultwarden
```bash
# Full deployment
ansible-playbook rick-infra.yml --tags vaultwarden
# Or via site.yml
ansible-playbook site.yml --tags vaultwarden -l homelab
```
### Access Admin Panel
1. Set admin token in vault file (plain text):
```yaml
# Generate a secure token
vault_vaultwarden_admin_token: "$(openssl rand -base64 32)"
```
2. The role automatically hashes the token during deployment:
- Hashing occurs on the **control node** using `argon2` CLI
- Uses OWASP recommended settings (19MiB memory, 2 iterations, 1 thread)
- Idempotent: same token always produces the same hash
- The `argon2` package is automatically installed if not present
3. Access: `https://vault.jnss.me/admin` (use the plain text token from step 1)
### Configure SSO (Authentik Integration)
> ⚠️ **IMPORTANT: SSO Feature Status (as of December 2025)**
>
> SSO is currently **only available in `vaultwarden/server:testing` images**.
> The stable release (v1.34.3) does **NOT** include SSO functionality.
>
> **Current Deployment Status:**
> - This role is configured with SSO settings ready for when SSO reaches stable release
> - Using `vaultwarden_version: "latest"` (stable) - SSO will not appear
> - To test SSO now: Set `vaultwarden_version: "testing"` (not recommended for production)
> - To wait for stable: Keep current configuration, SSO will activate automatically when available
>
> **References:**
> - [Vaultwarden Wiki - SSO Documentation](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect)
> - [Vaultwarden Testing Features](https://github.com/dani-garcia/vaultwarden/wiki#testing-features)
>
> **Decision:** This deployment keeps SSO configured but uses stable image until SSO feature is production-ready.
Following the [Authentik integration guide](https://integrations.goauthentik.io/security/vaultwarden/):
1. **In Authentik**: Create OAuth2/OpenID Provider
- **Name**: `Vaultwarden`
- **Client Type**: `Confidential`
- **Redirect URIs**: `https://vault.jnss.me/identity/connect/oidc-signin` (must be strict/exact match)
- **Scopes**: Under "Advanced protocol settings", ensure these scope mappings are selected:
- `authentik default OAuth Mapping: OpenID 'openid'`
- `authentik default OAuth Mapping: OpenID 'email'`
- `authentik default OAuth Mapping: OpenID 'profile'`
- `authentik default OAuth Mapping: OpenID 'offline_access'` ⚠️ **Required**
- **Access token validity**: Set to more than 5 minutes
- **Note the Client ID, Client Secret, and application slug** (from URL or provider settings)
2. **Update Vault Variables**:
```yaml
vault_vaultwarden_sso_client_id: "<client-id-from-authentik>"
vault_vaultwarden_sso_client_secret: "<client-secret-from-authentik>"
```
3. **Enable SSO and set authority** in `group_vars/homelab/main.yml`:
```yaml
vaultwarden_sso_enabled: true
# Replace 'vaultwarden' with your actual application slug
vaultwarden_sso_authority: "https://auth.jnss.me/application/o/vaultwarden/"
```
4. **Optional: SSO-Only Mode** (disable password login):
```yaml
vaultwarden_sso_only: true # Requires SSO, disables email+password
```
5. **Redeploy**:
```bash
ansible-playbook rick-infra.yml --tags vaultwarden
```
6. **Test**: Log out, enter a verified email on login page, click "Use single sign-on"
- **Note**: With `vaultwarden_version: "latest"`, SSO button will not appear (feature not in stable yet)
## Security Considerations
### Database Access
- Uses PostgreSQL Unix socket with **777 permissions**
- Security maintained via password authentication (scram-sha-256)
- See: `docs/socket-permissions-architecture.md`
### Admin Token
- **Never commit plain admin token to git**
- Use Ansible Vault for `vault_vaultwarden_admin_token`
- Rotate periodically via admin panel
### User Registration
- Default: **Disabled** (`vaultwarden_signups_allowed: false`)
- Users must be invited by existing users or created via admin panel
- Prevents unauthorized account creation
## Maintenance
### Backup
Backup the following:
```bash
# Database backup (via PostgreSQL role)
sudo -u postgres pg_dump vaultwarden > vaultwarden-backup.sql
# Data directory (attachments, icons, etc.)
tar -czf vaultwarden-data-backup.tar.gz /opt/vaultwarden/data
```
### Update Container
```bash
# Pull new image and restart
ansible-playbook rick-infra.yml --tags vaultwarden
```
### View Logs
```bash
# Service logs
journalctl -u vaultwarden -f
# Container logs
podman logs vaultwarden -f
```
### Restart Service
```bash
systemctl restart vaultwarden
```
## Troubleshooting
### Container won't start
```bash
# Check container status
systemctl status vaultwarden
# Check container directly
podman ps -a
podman logs vaultwarden
# Verify database connectivity
sudo -u vaultwarden psql -h /var/run/postgresql -U vaultwarden -d vaultwarden -c "SELECT 1;"
```
### Database connection errors
1. Verify PostgreSQL is running: `systemctl status postgresql`
2. Check socket exists: `ls -la /var/run/postgresql/.s.PGSQL.5432`
3. Verify socket permissions: Should be `srwxrwxrwx` (777)
4. Test connection as vaultwarden user (see above)
### Can't access admin panel
1. Verify admin token is set in vault file (plain text)
2. Check that the token was hashed successfully during deployment
3. Ensure you're using the plain text token to log in
4. Redeploy to regenerate hash if needed
### SSO not appearing / not working
**Most Common Issue: Using Stable Image**
SSO is only available in testing images. Check your deployment:
```bash
# Check current image version
podman inspect vaultwarden --format '{{.ImageName}}'
# Check API config for SSO support
curl -s http://127.0.0.1:8080/api/config | grep -o '"sso":"[^"]*"'
# Empty string "" = SSO not available in this image
# URL present = SSO is available
```
**If using `vaultwarden_version: "latest"`**: SSO will not appear (feature not in stable yet)
- **To test SSO**: Set `vaultwarden_version: "testing"` in role defaults or group/host vars
- **For production**: Wait for SSO to reach stable release (recommended)
**If using `vaultwarden_version: "testing"` and SSO still not working**:
1. Verify Authentik provider configuration:
- Check that `offline_access` scope mapping is added
- Verify redirect URI matches exactly: `https://vault.jnss.me/identity/connect/oidc-signin`
- Ensure access token validity is > 5 minutes
2. Verify SSO authority URL includes full path with slug:
- Should be: `https://auth.jnss.me/application/o/<your-slug>/`
- Not just: `https://auth.jnss.me`
3. Check client ID and secret in vault match Authentik
4. Verify all required scopes: `openid email profile offline_access`
5. Check Vaultwarden logs for SSO-related errors:
```bash
podman logs vaultwarden 2>&1 | grep -i sso
```
6. Test SSO flow: Log out, enter verified email, click "Use single sign-on"
## File Structure
```
roles/vaultwarden/
├── defaults/
│ └── main.yml # Default variables
├── handlers/
│ └── main.yml # Service restart handlers
├── meta/
│ └── main.yml # Role dependencies
├── tasks/
│ ├── main.yml # Main orchestration
│ ├── user.yml # User and directory setup
│ └── database.yml # PostgreSQL setup
├── templates/
│ ├── vaultwarden.container # Quadlet container definition
│ ├── vaultwarden.env.j2 # Environment configuration
│ └── vaultwarden.caddy.j2 # Caddy reverse proxy config
└── README.md # This file
```
## References
- [Vaultwarden Documentation](https://github.com/dani-garcia/vaultwarden/wiki)
- [PostgreSQL Backend Guide](https://github.com/dani-garcia/vaultwarden/wiki/Using-the-PostgreSQL-Backend)
- [SSO Configuration](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect)
- [Socket Permissions Architecture](../../docs/socket-permissions-architecture.md)
## License
MIT

View File

@@ -0,0 +1,171 @@
# Vaultwarden Role - Required Vault Variables
This document lists all vault-encrypted variables required by the Vaultwarden role.
## Required Variables
These variables **must** be defined in your vault file (e.g., `group_vars/homelab/vault.yml`):
### Database Credentials
```yaml
# PostgreSQL database password for vaultwarden user
vault_vaultwarden_db_password: "your-secure-database-password-here"
```
**Generation**:
```bash
openssl rand -base64 32
```
### Admin Panel Access
```yaml
# Plain text admin token (will be hashed during deployment)
vault_vaultwarden_admin_token: "your-secret-admin-token"
```
**Generation**:
```bash
# Generate a secure random token
openssl rand -base64 32
```
**Important**:
- Store as **plain text** in vault (the role will hash it automatically)
- Use the same token to access `/admin` panel
- The token is automatically hashed on the **control node** using argon2id
- Hashing uses OWASP recommended settings (m=19456, t=2, p=1)
- Hashing is **idempotent**: same token always produces same hash
- The `argon2` package is automatically installed if not present on control node
- Never commit the vault file unencrypted to git
## Optional Variables
These variables are only needed if you enable specific features:
### SMTP Configuration
Required if `vaultwarden_smtp_enabled: true`:
```yaml
# SMTP password for sending emails
vault_vaultwarden_smtp_password: "smtp-password-here"
```
### SSO Integration (Authentik)
> ⚠️ **SSO Feature Status (December 2025)**
>
> SSO is only available in `vaultwarden/server:testing` images (not in stable yet).
> This role is configured with SSO ready for when it reaches stable release.
>
> Current deployment uses `vaultwarden_version: "latest"` (stable) - SSO credentials
> below are configured but SSO will not appear until feature reaches stable.
Required if `vaultwarden_sso_enabled: true`:
```yaml
# OAuth2 Client ID from Authentik
vault_vaultwarden_sso_client_id: "your-client-id-here"
# OAuth2 Client Secret from Authentik
vault_vaultwarden_sso_client_secret: "your-client-secret-here"
```
**Setup** (following [Authentik integration guide](https://integrations.goauthentik.io/security/vaultwarden/)):
1. Create OAuth2/OpenID Provider in Authentik:
- Redirect URI: `https://vault.jnss.me/identity/connect/oidc-signin` (exact match)
- Add scope mappings: `openid`, `email`, `profile`, `offline_access` (required)
- Access token validity: > 5 minutes
- Note the **application slug** from the provider URL
2. Copy Client ID and Secret from Authentik
3. Add credentials to vault file
4. Set the SSO authority URL in role configuration:
```yaml
vaultwarden_sso_enabled: true
vaultwarden_sso_authority: "https://auth.jnss.me/application/o/<your-slug>/"
```
5. Deploy the role
6. **Wait for SSO to reach stable**, or use `vaultwarden_version: "testing"` to test now
**Important**:
- The SSO authority must include the full path with application slug
- The `offline_access` scope mapping is **required** for Vaultwarden SSO
- Access token must be valid for more than 5 minutes
- SSO is configured and ready but will activate when stable release includes it
## Example Vault File
```yaml
---
# group_vars/homelab/vault.yml (encrypted with ansible-vault)
# Vaultwarden - Database
vault_vaultwarden_db_password: "xK9mP2nR5tY8wQ3vZ7cB6sA4dF1gH0jL"
# Vaultwarden - Admin Panel (plain text, will be hashed automatically)
vault_vaultwarden_admin_token: "MySecureAdminToken123!"
# Vaultwarden - SMTP (optional)
vault_vaultwarden_smtp_password: "smtp-app-password-here"
# Vaultwarden - SSO (optional)
vault_vaultwarden_sso_client_id: "vaultwarden"
vault_vaultwarden_sso_client_secret: "sso-secret-from-authentik"
```
## Vault File Management
### Encrypt Vault File
```bash
ansible-vault encrypt group_vars/homelab/vault.yml
```
### Edit Vault File
```bash
ansible-vault edit group_vars/homelab/vault.yml
```
### Decrypt Vault File (temporary)
```bash
ansible-vault decrypt group_vars/homelab/vault.yml
# Make changes
ansible-vault encrypt group_vars/homelab/vault.yml
```
## Security Best Practices
1. **Never commit unencrypted vault files**
2. **Use strong passwords** (32+ characters for database, admin token)
3. **Rotate credentials periodically** (especially admin token)
4. **Limit vault password access** (use password manager)
5. **Use separate passwords** for different services
6. **Back up vault password** (secure location, not in git)
## Verifying Variables
Test if variables are properly loaded:
```bash
ansible -m debug -a "var=vault_vaultwarden_db_password" homelab --ask-vault-pass
```
## Troubleshooting
### Variable not found error
- Ensure vault file is in correct location: `group_vars/homelab/vault.yml`
- Verify file is encrypted: `file group_vars/homelab/vault.yml`
- Check variable name matches exactly (case-sensitive)
- Provide vault password with `--ask-vault-pass`
### Admin token not working
- Verify the plain text token in vault matches what you're entering
- Check for extra whitespace in vault file
- Ensure the token was hashed successfully during deployment (check ansible output)
- Try redeploying the role to regenerate the hash

View File

@@ -0,0 +1,109 @@
---
# =================================================================
# Vaultwarden Password Manager Role - Default Variables
# =================================================================
# Self-contained Vaultwarden deployment with Podman and PostgreSQL
# =================================================================
# Service Configuration
# =================================================================
# Service user and directories
vaultwarden_user: vaultwarden
vaultwarden_group: vaultwarden
vaultwarden_home: /opt/vaultwarden
vaultwarden_data_dir: "{{ vaultwarden_home }}/data"
# Container configuration
# NOTE: SSO feature is only available in "testing" tag (as of Dec 2025)
# Using "latest" (stable) means SSO will not appear even if configured
# SSO settings below are configured and ready for when feature reaches stable
vaultwarden_version: "latest"
vaultwarden_image: "vaultwarden/server"
# Service management
vaultwarden_service_enabled: true
vaultwarden_service_state: "started"
# =================================================================
# Database Configuration (Self-managed)
# =================================================================
vaultwarden_db_name: "vaultwarden"
vaultwarden_db_user: "vaultwarden"
vaultwarden_db_password: "{{ vault_vaultwarden_db_password }}"
# =================================================================
# Network Configuration
# =================================================================
vaultwarden_domain: "vault.jnss.me"
vaultwarden_http_port: 8080
# =================================================================
# Vaultwarden Core Configuration
# =================================================================
# Admin panel access token (plain text, will be hashed during deployment)
vaultwarden_admin_token_plain: "{{ vault_vaultwarden_admin_token }}"
# Registration and invitation controls
vaultwarden_signups_allowed: false # Disable open registration
vaultwarden_invitations_allowed: true # Allow existing users to invite
vaultwarden_show_password_hint: false # Don't show password hints
# WebSocket support for live sync
vaultwarden_websocket_enabled: true
# =================================================================
# Email Configuration (Optional)
# =================================================================
vaultwarden_smtp_enabled: true
vaultwarden_smtp_host: "smtp.titan.email"
vaultwarden_smtp_port: 587
vaultwarden_smtp_from: "hello@jnss.me"
vaultwarden_smtp_username: "hello@jnss.me"
vaultwarden_smtp_password: "{{ vault_smtp_password | default('') }}"
vaultwarden_smtp_security: "starttls" # Options: starttls, force_tls, off
# =================================================================
# SSO Configuration (Optional - Authentik Integration)
# =================================================================
vaultwarden_sso_enabled: false
# SSO Provider Configuration (Authentik)
vaultwarden_sso_client_id: "{{ vault_vaultwarden_sso_client_id | default('') }}"
vaultwarden_sso_client_secret: "{{ vault_vaultwarden_sso_client_secret | default('') }}"
# Authority must include full path with application slug
vaultwarden_sso_authority: "https://{{ authentik_domain }}/application/o/vaultwarden/"
vaultwarden_sso_scopes: "openid email profile offline_access"
# Additional SSO settings (per Authentik integration guide)
vaultwarden_sso_only: false # Set to true to disable email+password login and require SSO
vaultwarden_sso_signups_match_email: true # Match first SSO login to existing account by email
vaultwarden_sso_allow_unknown_email_verification: false
vaultwarden_sso_client_cache_expiration: 0
# Domain whitelist for SSO signups (comma-separated domains, empty = all)
vaultwarden_sso_signups_domains_whitelist: ""
# =================================================================
# Caddy Integration
# =================================================================
# Caddy configuration (assumes caddy role provides these variables)
caddy_sites_enabled_dir: "/etc/caddy/sites-enabled"
caddy_log_dir: "/var/log/caddy"
caddy_user: "caddy"
# =================================================================
# Infrastructure Dependencies (Read-only)
# =================================================================
# PostgreSQL socket configuration (managed by postgresql role)
postgresql_unix_socket_directories: "/var/run/postgresql"
postgresql_client_group: "postgres-clients"
postgresql_port: 5432
postgresql_unix_socket_enabled: true

View File

@@ -0,0 +1,16 @@
---
# Vaultwarden Password Manager - Service Handlers
- name: reload systemd
systemd:
daemon_reload: true
- name: restart vaultwarden
systemd:
name: vaultwarden
state: restarted
- name: reload caddy
systemd:
name: caddy
state: reloaded

View File

@@ -0,0 +1,25 @@
---
# Vaultwarden Password Manager - Role Metadata
dependencies:
- role: postgresql
- role: caddy
galaxy_info:
author: Rick Infrastructure Team
description: Vaultwarden password manager deployment with PostgreSQL and Caddy
license: MIT
min_ansible_version: "2.14"
platforms:
- name: ArchLinux
versions:
- all
galaxy_tags:
- vaultwarden
- bitwarden
- password-manager
- security
- postgresql
- podman

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