Compare commits
20 Commits
4f8da38ca6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f3f111d88 | |||
| eca3b4a726 | |||
| 9bb9405b90 | |||
| bfd6f22f0e | |||
| 89b43180fc | |||
| 846ab74f87 | |||
| 90bbcd97b1 | |||
| 1be7122251 | |||
| 467e79c84b | |||
| cf71fb3a8d | |||
| 2fe194ba82 | |||
| 9b12225ec8 | |||
| f40349c2e7 | |||
| 4f8b46fa14 | |||
| a9f814d929 | |||
| 44584c68f1 | |||
| 0ecbb84fa5 | |||
| 1350d10a7c | |||
| ecbeb07ba2 | |||
| e8b76c6a72 |
74
README.md
74
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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/
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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`:
|
||||||
|
|
||||||
|
|||||||
414
docs/devigo-deployment-guide.md
Normal file
414
docs/devigo-deployment-guide.md
Normal 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)
|
||||||
541
docs/gitea-deployment-guide.md
Normal file
541
docs/gitea-deployment-guide.md
Normal 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.
|
||||||
211
docs/gitea-email-troubleshooting.md
Normal file
211
docs/gitea-email-troubleshooting.md
Normal 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
|
||||||
245
docs/gitea-ssh-migration-guide.md
Normal file
245
docs/gitea-ssh-migration-guide.md
Normal 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
326
docs/jnss-web-deployment.md
Normal 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
|
||||||
311
docs/metrics-deployment-guide.md
Normal file
311
docs/metrics-deployment-guide.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
367
docs/service-domain-configuration.md
Normal file
367
docs/service-domain-configuration.md
Normal 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.
|
||||||
@@ -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)
|
||||||
307
docs/vaultwarden-sso-status.md
Normal file
307
docs/vaultwarden-sso-status.md
Normal 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.
|
||||||
71
group_vars/production/main.yml
Normal file
71
group_vars/production/main.yml
Normal 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
|
||||||
@@ -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
|
||||||
# =================================================================
|
# =================================================================
|
||||||
|
|||||||
0
host_vars/mini-vps/main.yml
Normal file
0
host_vars/mini-vps/main.yml
Normal 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
31
now-what.md
Normal 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"
|
||||||
|
|
||||||
24
playbooks/backup-sigvild.yml
Normal file
24
playbooks/backup-sigvild.yml
Normal 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
|
||||||
133
playbooks/deploy-jnss-web.yml
Normal file
133
playbooks/deploy-jnss-web.yml
Normal 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
|
||||||
203
playbooks/remove-nextcloud.yml
Normal file
203
playbooks/remove-nextcloud.yml
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
57
playbooks/templates/jnss-web.caddy.j2
Normal file
57
playbooks/templates/jnss-web.caddy.j2
Normal 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
33
production.yml
Normal 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
55
rick-infra.yml
Normal 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']
|
||||||
|
|
||||||
@@ -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 }}"
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
612
roles/devigo/README.md
Normal 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.
|
||||||
30
roles/devigo/defaults/main.yml
Normal file
30
roles/devigo/defaults/main.yml
Normal 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
|
||||||
19
roles/devigo/handlers/main.yml
Normal file
19
roles/devigo/handlers/main.yml
Normal 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
|
||||||
4
roles/devigo/meta/main.yml
Normal file
4
roles/devigo/meta/main.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
dependencies:
|
||||||
|
- role: podman
|
||||||
|
- role: caddy
|
||||||
93
roles/devigo/tasks/main.yml
Normal file
93
roles/devigo/tasks/main.yml
Normal 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
|
||||||
32
roles/devigo/tasks/setup_infrastructure.yml
Normal file
32
roles/devigo/tasks/setup_infrastructure.yml
Normal 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
|
||||||
54
roles/devigo/tasks/setup_oauth_service.yml
Normal file
54
roles/devigo/tasks/setup_oauth_service.yml
Normal 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
|
||||||
25
roles/devigo/templates/devigo-decap-oauth.caddy.j2
Normal file
25
roles/devigo/templates/devigo-decap-oauth.caddy.j2
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
25
roles/devigo/templates/devigo-decap-oauth.container
Normal file
25
roles/devigo/templates/devigo-decap-oauth.container
Normal 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
|
||||||
10
roles/devigo/templates/devigo-decap-oauth.env.j2
Normal file
10
roles/devigo/templates/devigo-decap-oauth.env.j2
Normal 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 }}
|
||||||
31
roles/devigo/templates/devigo-site.container
Normal file
31
roles/devigo/templates/devigo-site.container
Normal 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
|
||||||
33
roles/devigo/templates/devigo.caddy.j2
Normal file
33
roles/devigo/templates/devigo.caddy.j2
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'
|
||||||
121
roles/gitea/tasks/fail2ban.yml
Normal file
121
roles/gitea/tasks/fail2ban.yml
Normal 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
|
||||||
51
roles/gitea/tasks/firewall.yml
Normal file
51
roles/gitea/tasks/firewall.yml
Normal 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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
74
roles/gitea/tasks/ssh_dedicated.yml
Normal file
74
roles/gitea/tasks/ssh_dedicated.yml
Normal 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
|
||||||
128
roles/gitea/tasks/ssh_passthrough.yml
Normal file
128
roles/gitea/tasks/ssh_passthrough.yml
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
28
roles/gitea/templates/gitea-keys.sh.j2
Normal file
28
roles/gitea/templates/gitea-keys.sh.j2
Normal 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
|
||||||
@@ -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 }}
|
||||||
|
|
||||||
|
|||||||
11
roles/gitea/templates/gitea.nft.j2
Normal file
11
roles/gitea/templates/gitea.nft.j2
Normal 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
325
roles/metrics/README.md
Normal 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/)
|
||||||
178
roles/metrics/defaults/main.yml
Normal file
178
roles/metrics/defaults/main.yml
Normal 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
|
||||||
23
roles/metrics/handlers/main.yml
Normal file
23
roles/metrics/handlers/main.yml
Normal 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
|
||||||
3
roles/metrics/meta/main.yml
Normal file
3
roles/metrics/meta/main.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
dependencies:
|
||||||
|
- role: caddy
|
||||||
9
roles/metrics/tasks/caddy.yml
Normal file
9
roles/metrics/tasks/caddy.yml
Normal 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
|
||||||
90
roles/metrics/tasks/grafana.yml
Normal file
90
roles/metrics/tasks/grafana.yml
Normal 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
|
||||||
20
roles/metrics/tasks/main.yml
Normal file
20
roles/metrics/tasks/main.yml
Normal 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]
|
||||||
49
roles/metrics/tasks/node_exporter.yml
Normal file
49
roles/metrics/tasks/node_exporter.yml
Normal 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
|
||||||
66
roles/metrics/tasks/victoriametrics.yml
Normal file
66
roles/metrics/tasks/victoriametrics.yml
Normal 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
|
||||||
12
roles/metrics/templates/dashboards.yml.j2
Normal file
12
roles/metrics/templates/dashboards.yml.j2
Normal 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
|
||||||
12
roles/metrics/templates/datasource-victoriametrics.yml.j2
Normal file
12
roles/metrics/templates/datasource-victoriametrics.yml.j2
Normal 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
|
||||||
26
roles/metrics/templates/grafana.caddy.j2
Normal file
26
roles/metrics/templates/grafana.caddy.j2
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
68
roles/metrics/templates/grafana.ini.j2
Normal file
68
roles/metrics/templates/grafana.ini.j2
Normal 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
|
||||||
36
roles/metrics/templates/grafana.service.j2
Normal file
36
roles/metrics/templates/grafana.service.j2
Normal 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
|
||||||
42
roles/metrics/templates/node_exporter.service.j2
Normal file
42
roles/metrics/templates/node_exporter.service.j2
Normal 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
|
||||||
22
roles/metrics/templates/scrape.yml.j2
Normal file
22
roles/metrics/templates/scrape.yml.j2
Normal 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 }}'
|
||||||
41
roles/metrics/templates/victoriametrics.service.j2
Normal file
41
roles/metrics/templates/victoriametrics.service.j2
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
# =================================================================
|
# =================================================================
|
||||||
|
|||||||
36
roles/nextcloud/tasks/configure.yml
Normal file
36
roles/nextcloud/tasks/configure.yml
Normal 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]
|
||||||
72
roles/nextcloud/tasks/cron.yml
Normal file
72
roles/nextcloud/tasks/cron.yml
Normal 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"
|
||||||
@@ -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]
|
||||||
|
|||||||
189
roles/nextcloud/templates/configure-nextcloud.sh.j2
Normal file
189
roles/nextcloud/templates/configure-nextcloud.sh.j2
Normal 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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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 }}',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
# =================================================================
|
# =================================================================
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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!
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
# =================================================================
|
# =================================================================
|
||||||
|
|||||||
32
roles/podman/templates/podman.nft.j2
Normal file
32
roles/podman/templates/podman.nft.j2
Normal 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)"
|
||||||
@@ -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: |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
331
roles/vaultwarden/README.md
Normal 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
|
||||||
171
roles/vaultwarden/VAULT_VARIABLES.md
Normal file
171
roles/vaultwarden/VAULT_VARIABLES.md
Normal 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
|
||||||
109
roles/vaultwarden/defaults/main.yml
Normal file
109
roles/vaultwarden/defaults/main.yml
Normal 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
|
||||||
16
roles/vaultwarden/handlers/main.yml
Normal file
16
roles/vaultwarden/handlers/main.yml
Normal 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
|
||||||
25
roles/vaultwarden/meta/main.yml
Normal file
25
roles/vaultwarden/meta/main.yml
Normal 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
Reference in New Issue
Block a user