From 1350d10a7cf51758ee5e31ca6339f24d72085d76 Mon Sep 17 00:00:00 2001 From: Joakim Date: Tue, 16 Dec 2025 00:53:33 +0100 Subject: [PATCH] Add devigo deployment role for mini-vps production environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created comprehensive devigo Ansible role with Podman Quadlet support - Deployed devigo-site container (Hugo + nginx) via systemd - Deployed devigo-decap-oauth OAuth2 proxy for Decap CMS - Integrated with Caddy reverse proxy for HTTPS Services deployed: - devigo.no (apex domain, primary) - www.devigo.no (redirects to apex) - decap.jnss.me (OAuth proxy) Key features: - REGISTRY_AUTH_FILE environment for Podman GHCR authentication - TRUSTED_ORIGINS (plural) for decapcms-oauth2 multi-origin support - JavaScript-based Decap CMS initialization (eliminates YAML MIME dependency) - nginx location block for YAML MIME type (text/yaml) - Automated deployment via GitHub Actions CI/CD - Comprehensive documentation with troubleshooting guide - Architecture decision records Fixes applied during deployment: - OAuth origin trust validation (TRUSTED_ORIGINS vs TRUSTED_ORIGIN) - MIME type handling strategy (location-specific vs server-level types block) - Decap CMS initialization method (JavaScript vs link tag) - Podman authentication for systemd services (REGISTRY_AUTH_FILE) Testing status: - ✅ MIME types verified (HTML, CSS, YAML all correct) - ✅ OAuth authentication working - ✅ Container image pulls from private GHCR - ✅ Automated deployments functional - ✅ Site fully operational at devigo.no --- roles/devigo/README.md | 612 ++++++++++++++++++ roles/devigo/defaults/main.yml | 30 + roles/devigo/handlers/main.yml | 19 + roles/devigo/meta/main.yml | 4 + roles/devigo/tasks/main.yml | 93 +++ roles/devigo/tasks/setup_infrastructure.yml | 32 + roles/devigo/tasks/setup_oauth_service.yml | 54 ++ .../templates/devigo-decap-oauth.caddy.j2 | 25 + .../templates/devigo-decap-oauth.container | 25 + .../templates/devigo-decap-oauth.env.j2 | 10 + roles/devigo/templates/devigo-site.container | 31 + roles/devigo/templates/devigo.caddy.j2 | 33 + 12 files changed, 968 insertions(+) create mode 100644 roles/devigo/README.md create mode 100644 roles/devigo/defaults/main.yml create mode 100644 roles/devigo/handlers/main.yml create mode 100644 roles/devigo/meta/main.yml create mode 100644 roles/devigo/tasks/main.yml create mode 100644 roles/devigo/tasks/setup_infrastructure.yml create mode 100644 roles/devigo/tasks/setup_oauth_service.yml create mode 100644 roles/devigo/templates/devigo-decap-oauth.caddy.j2 create mode 100644 roles/devigo/templates/devigo-decap-oauth.container create mode 100644 roles/devigo/templates/devigo-decap-oauth.env.j2 create mode 100644 roles/devigo/templates/devigo-site.container create mode 100644 roles/devigo/templates/devigo.caddy.j2 diff --git a/roles/devigo/README.md b/roles/devigo/README.md new file mode 100644 index 0000000..f926482 --- /dev/null +++ b/roles/devigo/README.md @@ -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 `` tag + +**Rationale**: +- Eliminates dependency on YAML MIME type for `` 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. diff --git a/roles/devigo/defaults/main.yml b/roles/devigo/defaults/main.yml new file mode 100644 index 0000000..d6f2510 --- /dev/null +++ b/roles/devigo/defaults/main.yml @@ -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 diff --git a/roles/devigo/handlers/main.yml b/roles/devigo/handlers/main.yml new file mode 100644 index 0000000..06adcad --- /dev/null +++ b/roles/devigo/handlers/main.yml @@ -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 diff --git a/roles/devigo/meta/main.yml b/roles/devigo/meta/main.yml new file mode 100644 index 0000000..de16f95 --- /dev/null +++ b/roles/devigo/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: podman + - role: caddy diff --git a/roles/devigo/tasks/main.yml b/roles/devigo/tasks/main.yml new file mode 100644 index 0000000..499617e --- /dev/null +++ b/roles/devigo/tasks/main.yml @@ -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 diff --git a/roles/devigo/tasks/setup_infrastructure.yml b/roles/devigo/tasks/setup_infrastructure.yml new file mode 100644 index 0000000..e482271 --- /dev/null +++ b/roles/devigo/tasks/setup_infrastructure.yml @@ -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 diff --git a/roles/devigo/tasks/setup_oauth_service.yml b/roles/devigo/tasks/setup_oauth_service.yml new file mode 100644 index 0000000..f57f9ba --- /dev/null +++ b/roles/devigo/tasks/setup_oauth_service.yml @@ -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 diff --git a/roles/devigo/templates/devigo-decap-oauth.caddy.j2 b/roles/devigo/templates/devigo-decap-oauth.caddy.j2 new file mode 100644 index 0000000..89eee33 --- /dev/null +++ b/roles/devigo/templates/devigo-decap-oauth.caddy.j2 @@ -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 + } +} diff --git a/roles/devigo/templates/devigo-decap-oauth.container b/roles/devigo/templates/devigo-decap-oauth.container new file mode 100644 index 0000000..075f318 --- /dev/null +++ b/roles/devigo/templates/devigo-decap-oauth.container @@ -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 diff --git a/roles/devigo/templates/devigo-decap-oauth.env.j2 b/roles/devigo/templates/devigo-decap-oauth.env.j2 new file mode 100644 index 0000000..c306a2f --- /dev/null +++ b/roles/devigo/templates/devigo-decap-oauth.env.j2 @@ -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 }} diff --git a/roles/devigo/templates/devigo-site.container b/roles/devigo/templates/devigo-site.container new file mode 100644 index 0000000..34856c3 --- /dev/null +++ b/roles/devigo/templates/devigo-site.container @@ -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 diff --git a/roles/devigo/templates/devigo.caddy.j2 b/roles/devigo/templates/devigo.caddy.j2 new file mode 100644 index 0000000..540cf76 --- /dev/null +++ b/roles/devigo/templates/devigo.caddy.j2 @@ -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 + } +}