diff --git a/ansible.cfg b/ansible.cfg index 065ff4d..772b1be 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -4,6 +4,7 @@ host_key_checking = False remote_user = root deprecation_warnings = False vault_password_file = vault-password-file +roles_path = roles [ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s diff --git a/docs/sigvild-gallery-deployment.md b/docs/sigvild-gallery-deployment.md new file mode 100644 index 0000000..578c9e6 --- /dev/null +++ b/docs/sigvild-gallery-deployment.md @@ -0,0 +1,262 @@ +# Sigvild Gallery Deployment Guide + +## Quick Start + +Deploy the complete Sigvild Wedding Gallery with PocketBase API and SvelteKit frontend. + +## Prerequisites Setup + +### 1. Vault Password Configuration + +Create encrypted passwords for the gallery authentication: + +```bash +# Create vault passwords (run from rick-infra directory) +ansible-vault encrypt_string 'your-host-password-here' --name 'vault_sigvild_host_password' +ansible-vault encrypt_string 'your-guest-password-here' --name 'vault_sigvild_guest_password' +``` + +Add the encrypted strings to `host_vars/arch-vps/main.yml`: + +```yaml +# Add to host_vars/arch-vps/main.yml +vault_sigvild_host_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 66386439653765386... + +vault_sigvild_guest_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 33663065383834313... +``` + +### 2. DNS Configuration + +Ensure these domains point to your server: +- `sigvild.no` → Frontend static site +- `api.sigvild.no` → API backend proxy + +### 3. Project Structure + +Ensure the sigvild-gallery project is adjacent to rick-infra: + +``` +~/ +├── rick-infra/ # This repository +└── sigvild-gallery/ # Sigvild gallery project + ├── build_tmp/ # Production builds + ├── sigvild-kit/ # Frontend source + └── main.go # Backend source +``` + +## Deployment Commands + +### Full Infrastructure + Gallery + +Deploy everything including Sigvild Gallery: + +```bash +ansible-playbook site.yml +``` + +### Gallery Only + +Deploy just the Sigvild Gallery service: + +```bash +ansible-playbook playbooks/deploy-sigvild.yml +``` + +### Selective Updates + +Update specific components: + +```bash +# Frontend only (quick static file updates) +ansible-playbook site.yml --tags="frontend" + +# Backend only (API service updates) +ansible-playbook site.yml --tags="backend" + +# Caddy configuration only +ansible-playbook site.yml --tags="caddy" + +# Just build process (development) +ansible-playbook site.yml --tags="build" +``` + +## Architecture Overview + +``` +Internet + ↓ +Caddy (Auto HTTPS) + ├── sigvild.no → /var/www/sigvild-gallery/ (Static Files) + └── api.sigvild.no → localhost:8090 (PocketBase API) + ↓ + Go Binary (sigvild-gallery-server) + ↓ + SQLite Database + File Storage +``` + +## Service Management + +### Status Checks + +```bash +# Gallery API service +systemctl status sigvild-gallery + +# Caddy web server +systemctl status caddy + +# View gallery logs +journalctl -u sigvild-gallery -f + +# View Caddy logs +journalctl -u caddy -f +``` + +### Manual Operations + +```bash +# Restart gallery service +systemctl restart sigvild-gallery + +# Reload Caddy configuration +systemctl reload caddy + +# Check API health +curl https://api.sigvild.no/api/health +``` + +## Troubleshooting + +### Build Issues + +**Problem**: Go build fails +```bash +# Ensure Go is installed locally +go version + +# Check if you're in the right directory +ls sigvild-gallery/main.go +``` + +**Problem**: Frontend build fails +```bash +# Check Node.js and npm +node --version && npm --version + +# Ensure dependencies are installed +cd sigvild-gallery/sigvild-kit +npm install +``` + +### Service Issues + +**Problem**: Service won't start +```bash +# Check service status +systemctl status sigvild-gallery + +# Check service logs +journalctl -u sigvild-gallery --no-pager + +# Verify binary permissions +ls -la /opt/sigvild-gallery/sigvild-gallery-server +``` + +**Problem**: Database permissions +```bash +# Check data directory ownership +ls -la /opt/sigvild-gallery/data/ + +# Fix ownership if needed +sudo chown -R sigvild:sigvild /opt/sigvild-gallery/ +``` + +### Network Issues + +**Problem**: Domain not resolving +```bash +# Test DNS resolution +dig sigvild.no +dig api.sigvild.no + +# Test local connectivity +curl -H "Host: sigvild.no" http://localhost +curl -H "Host: api.sigvild.no" http://localhost +``` + +**Problem**: HTTPS certificate issues +```bash +# Check Caddy logs for ACME errors +journalctl -u caddy | grep -i "acme\|certificate" + +# Verify DNS challenge credentials +# (Check Cloudflare API token in vault) +``` + +## Security Features + +### Environment Protection +- **No .env files**: Secrets stored in systemd environment variables only +- **Vault encryption**: All passwords encrypted with ansible-vault +- **Memory isolation**: Secrets only exist in process memory + +### SystemD Sandboxing +- **Read-only filesystem**: Application cannot modify system files +- **Isolated temporary**: Private /tmp directory +- **Limited capabilities**: No privilege escalation possible +- **Data directory only**: Write access restricted to /opt/sigvild-gallery/data/ + +### Web Security +- **Automatic HTTPS**: Let's Encrypt certificates via DNS challenge +- **Security headers**: XSS protection, frame options, content type sniffing prevention +- **CORS restrictions**: API access limited to frontend domain +- **Rate limiting**: API endpoint protection + +## File Locations + +### Application Files +- **Binary**: `/opt/sigvild-gallery/sigvild-gallery-server` +- **Database**: `/opt/sigvild-gallery/data/data.db` +- **File uploads**: `/opt/sigvild-gallery/data/storage/` +- **Frontend**: `/var/www/sigvild-gallery/` + +### Configuration Files +- **Service**: `/etc/systemd/system/sigvild-gallery.service` +- **Caddy frontend**: `/etc/caddy/sites-enabled/sigvild-frontend.caddy` +- **Caddy API**: `/etc/caddy/sites-enabled/sigvild-api.caddy` + +### Log Files +- **Service logs**: `journalctl -u sigvild-gallery` +- **Caddy logs**: `journalctl -u caddy` +- **Access logs**: `/var/log/caddy/sigvild-*.log` + +## Next Steps After Deployment + +1. **Verify services**: Check that both domains are accessible +2. **Test authentication**: Login with host/guest credentials +3. **Upload test photo**: Verify file upload functionality +4. **Monitor logs**: Watch for any errors in service logs +5. **Backup setup**: Configure regular database backups + +## Development Workflow + +For ongoing development: + +```bash +# 1. Make changes to sigvild-gallery project +cd ../sigvild-gallery + +# 2. Test locally +go run . serve & +cd sigvild-kit && npm run dev + +# 3. Deploy updates +cd ../rick-infra +ansible-playbook site.yml --tags="sigvild" +``` + +The deployment system builds locally and transfers assets, so you don't need build tools on the server. \ No newline at end of file diff --git a/host_vars/arch-vps/main.yml b/host_vars/arch-vps/main.yml index 7010a38..313b892 100644 --- a/host_vars/arch-vps/main.yml +++ b/host_vars/arch-vps/main.yml @@ -35,9 +35,23 @@ caddy_server_name: "main" # service_backend: "localhost:8080" # notify: register service with caddy +# ================================================================= +# Sigvild Gallery Configuration +# ================================================================= +sigvild_gallery_frontend_domain: "sigvild.no" +sigvild_gallery_api_domain: "api.sigvild.no" + +sigvild_gallery_local_project_path: "~/sigvild-gallery/" + +# 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 }}" + # ================================================================= # Security & Logging # ================================================================= caddy_log_level: "INFO" caddy_log_format: "json" -caddy_systemd_security: true \ No newline at end of file +caddy_systemd_security: true diff --git a/playbooks/deploy-sigvild.yml b/playbooks/deploy-sigvild.yml new file mode 100644 index 0000000..0e956d7 --- /dev/null +++ b/playbooks/deploy-sigvild.yml @@ -0,0 +1,52 @@ +--- +# Sigvild Gallery Deployment Playbook + +- name: Deploy Sigvild Wedding Gallery + hosts: arch-vps + become: yes + gather_facts: yes + + vars: + # Local project path - adjust as needed + sigvild_gallery_local_project_path: "{{ ansible_env.PWD }}/../sigvild-gallery" + + pre_tasks: + - name: Verify local sigvild-gallery project exists + local_action: + module: stat + path: "{{ sigvild_gallery_local_project_path }}" + register: project_exists + become: no + + - name: Fail if project directory doesn't exist + fail: + msg: "Sigvild Gallery project not found at {{ sigvild_gallery_local_project_path }}" + when: not project_exists.stat.exists + + - name: Display deployment information + debug: + msg: + - "Deploying Sigvild Gallery from: {{ sigvild_gallery_local_project_path }}" + - "Frontend domain: {{ sigvild_gallery_frontend_domain }}" + - "API domain: {{ sigvild_gallery_api_domain }}" + + roles: + - role: sigvild-gallery + tags: ['sigvild', 'gallery', 'wedding'] + + post_tasks: + - name: Wait for API to be ready + wait_for: + port: "{{ sigvild_gallery_port }}" + host: "{{ sigvild_gallery_host }}" + timeout: 60 + tags: [verify] + + - name: Display deployment results + debug: + msg: + - "✅ Sigvild Gallery deployment completed!" + - "Frontend: https://{{ sigvild_gallery_frontend_domain }}" + - "API: https://{{ sigvild_gallery_api_domain }}" + - "Service status: systemctl status sigvild-gallery" + - "Logs: journalctl -u sigvild-gallery -f" \ No newline at end of file diff --git a/roles/caddy/tasks/main.yml b/roles/caddy/tasks/main.yml index d2e3420..ef0c46c 100644 --- a/roles/caddy/tasks/main.yml +++ b/roles/caddy/tasks/main.yml @@ -3,19 +3,35 @@ set_fact: dns_challenge_needed: "{{ caddy_dns_provider == 'cloudflare' and cloudflare_api_token != '' }}" +- name: Check if Caddy is already installed + command: /usr/bin/caddy version + register: caddy_version_check + failed_when: false + changed_when: false + when: dns_challenge_needed | bool + +- name: Check if installed Caddy has Cloudflare plugin + command: /usr/bin/caddy list-modules --packages + register: caddy_modules_check + failed_when: false + changed_when: false + when: dns_challenge_needed | bool and caddy_version_check.rc == 0 + - name: Install standard Caddy (if no DNS challenge needed) pacman: name: caddy state: present - when: not dns_challenge_needed | bool + when: not dns_challenge_needed and not caddy_version_check | bool notify: restart caddy - name: Download Caddy with Cloudflare plugin (if DNS challenge needed) get_url: - url: "https://caddyserver.com/api/download?os=linux&arch=amd64&p=github.com/caddy-dns/cloudflare" - dest: /tmp/caddy-with-cloudflare - mode: '0755' - when: dns_challenge_needed | bool + url: "https://caddyserver.com/api/download?os=linux&arch=amd64&p=github.com/caddy-dns/cloudflare" + dest: /tmp/caddy-with-cloudflare + mode: '0755' + when: + - dns_challenge_needed | bool + - caddy_version_check.rc != 0 or 'github.com/caddy-dns/cloudflare' not in caddy_modules_check.stdout | default('') - name: Install Caddy with Cloudflare plugin copy: @@ -24,7 +40,7 @@ mode: '0755' remote_src: yes backup: yes - when: dns_challenge_needed | bool + when: dns_challenge_needed and caddy_version_check | bool notify: restart caddy - name: Clean up temporary Caddy binary diff --git a/roles/sigvild-gallery/README.md b/roles/sigvild-gallery/README.md new file mode 100644 index 0000000..6b093c4 --- /dev/null +++ b/roles/sigvild-gallery/README.md @@ -0,0 +1,180 @@ +# Sigvild Gallery Ansible Role + +Deploys the Sigvild Wedding Gallery application with PocketBase API backend and SvelteKit frontend. + +## Architecture + +- **Backend**: PocketBase-based Go application serving API on localhost:8090 +- **Frontend**: SvelteKit static site served by Caddy +- **Database**: SQLite via PocketBase (file-based storage) +- **Authentication**: Shared password system (host/guest users) +- **Domains**: + - `sigvild.no` → Frontend static files + - `api.sigvild.no` → Backend API proxy + +## Prerequisites + +- Caddy role deployed and configured +- Local sigvild-gallery project with built assets in `build_tmp/` +- Vault-encrypted passwords configured in inventory + +## Variables + +### Required Variables + +```yaml +# Domains +sigvild_gallery_frontend_domain: "sigvild.no" +sigvild_gallery_api_domain: "api.sigvild.no" + +# Vault-encrypted passwords +vault_sigvild_host_password: "your-encrypted-host-password" +vault_sigvild_guest_password: "your-encrypted-guest-password" +``` + +### Optional Variables + +```yaml +# Service configuration +sigvild_gallery_user: "sigvild" +sigvild_gallery_port: 8090 +sigvild_gallery_host: "127.0.0.1" + +# Paths +sigvild_gallery_home: "/opt/sigvild-gallery" +sigvild_gallery_web_root: "/var/www/sigvild-gallery" +sigvild_gallery_local_project_path: "{{ ansible_env.PWD }}/sigvild-gallery" +``` + +## Usage + +### Full Deployment + +```bash +# Deploy complete infrastructure including Sigvild Gallery +ansible-playbook site.yml + +# Deploy just Sigvild Gallery +ansible-playbook playbooks/deploy-sigvild.yml +``` + +### Selective Updates + +```bash +# Update just the frontend +ansible-playbook site.yml --tags="frontend" + +# Update just the backend API +ansible-playbook site.yml --tags="backend" + +# Update Caddy configuration +ansible-playbook site.yml --tags="caddy" +``` + +## Security Features + +### Environment Variables +- **No .env files**: Secrets managed via systemd Environment directives +- **Vault encrypted**: Passwords stored in Ansible vault +- **Memory-only**: Environment variables only exist in process memory + +### SystemD Sandboxing +- `NoNewPrivileges=yes`: Prevents privilege escalation +- `PrivateTmp=yes`: Isolated temporary directory +- `ProtectSystem=strict`: Read-only filesystem protection +- `ProtectHome=yes`: Home directory protection +- `ReadWritePaths`: Only data directory is writable + +### Caddy Security +- **Security headers**: XSS protection, frame options, content type sniffing prevention +- **CORS configuration**: Restricted to frontend domain +- **Rate limiting**: API endpoint protection +- **HTTPS only**: Automatic TLS with Let's Encrypt + +## Directory Structure + +``` +/opt/sigvild-gallery/ # Application home +├── sigvild-gallery-server # Go binary +└── data/ # PocketBase data directory + ├── data.db # SQLite database + └── storage/ # File uploads + +/var/www/sigvild-gallery/ # Frontend web root +├── index.html # SvelteKit build +├── _app/ # Application assets +└── assets/ # Static assets + +/etc/systemd/system/ +└── sigvild-gallery.service # SystemD service + +/etc/caddy/sites-enabled/ +├── sigvild-frontend.caddy # Frontend configuration +└── sigvild-api.caddy # API proxy configuration +``` + +## Build Process + +The role performs local builds then transfers assets: + +1. **Backend**: `GOOS=linux GOARCH=amd64 go build -o sigvild-gallery-server .` +2. **Frontend**: `npm run build` in `sigvild-kit/` directory +3. **Transfer**: Copy binary and sync frontend build to server +4. **Deploy**: Update systemd service and Caddy configuration + +## Service Management + +```bash +# Check service status +systemctl status sigvild-gallery + +# View logs +journalctl -u sigvild-gallery -f + +# Restart service +systemctl restart sigvild-gallery + +# Reload Caddy configuration +systemctl reload caddy +``` + +## Troubleshooting + +### Build Failures +- Ensure Go toolchain is available locally +- Verify `sigvild-kit/` directory exists with `package.json` +- Check Node.js and npm are installed for frontend builds + +### Service Startup Issues +- Check systemd logs: `journalctl -u sigvild-gallery` +- Verify binary permissions and ownership +- Ensure data directory is writable by service user + +### Domain Resolution +- Verify DNS records point to server IP +- Check Caddy logs: `journalctl -u caddy` +- Test local connectivity: `curl -H "Host: api.sigvild.no" http://localhost:8090` + +## Dependencies + +- **caddy**: Required for web server and reverse proxy +- **systemd**: Service management +- **Local build tools**: Go compiler, Node.js/npm + +## Files Created + +- `/etc/systemd/system/sigvild-gallery.service` +- `/etc/caddy/sites-enabled/sigvild-frontend.caddy` +- `/etc/caddy/sites-enabled/sigvild-api.caddy` +- `/opt/sigvild-gallery/` (application directory) +- `/var/www/sigvild-gallery/` (frontend files) + +## Tags + +- `sigvild`: Complete Sigvild Gallery deployment +- `backend`: API service deployment +- `frontend`: Static site deployment +- `build`: Local build processes +- `service`: SystemD service management +- `caddy`: Caddy configuration +- `verify`: Post-deployment verification \ No newline at end of file diff --git a/roles/sigvild-gallery/defaults/main.yml b/roles/sigvild-gallery/defaults/main.yml new file mode 100644 index 0000000..0515e04 --- /dev/null +++ b/roles/sigvild-gallery/defaults/main.yml @@ -0,0 +1,36 @@ +--- +# Sigvild Gallery Ansible Role - Default Variables + +# Service Configuration +sigvild_gallery_user: sigvild + +# Paths +sigvild_gallery_home: /opt/sigvild-gallery +sigvild_gallery_web_root: /var/www/sigvild-gallery +sigvild_gallery_binary: "{{ sigvild_gallery_home }}/sigvild-gallery" +sigvild_gallery_data_dir: "{{ sigvild_gallery_home }}/pb_data" + +# Domains +sigvild_gallery_frontend_domain: sigvild.no +sigvild_gallery_api_domain: api.sigvild.no + +# Backend Service +sigvild_gallery_port: 8090 +sigvild_gallery_host: "127.0.0.1" + +# Environment Variables (for SystemD service) +sigvild_gallery_host_username: host +sigvild_gallery_host_password: "{{ vault_sigvild_host_password }}" +sigvild_gallery_guest_username: guest +sigvild_gallery_guest_password: "{{ vault_sigvild_guest_password }}" + +# Build configuration +sigvild_gallery_local_project_path: "{{ ansible_env.PWD }}/sigvild-gallery" + +# Service configuration +sigvild_gallery_service_enabled: true +sigvild_gallery_service_state: started + +# Caddy integration (assumes caddy role provides these) +# caddy_sites_enabled_dir: /etc/caddy/sites-enabled +# caddy_user: caddy diff --git a/roles/sigvild-gallery/handlers/main.yml b/roles/sigvild-gallery/handlers/main.yml new file mode 100644 index 0000000..3901dc7 --- /dev/null +++ b/roles/sigvild-gallery/handlers/main.yml @@ -0,0 +1,16 @@ +--- +# Sigvild Gallery Handlers + +- name: reload systemd + systemd: + daemon_reload: yes + +- name: restart sigvild-gallery + systemd: + name: sigvild-gallery + state: restarted + +- name: reload caddy + systemd: + name: caddy + state: reloaded \ No newline at end of file diff --git a/roles/sigvild-gallery/meta/main.yml b/roles/sigvild-gallery/meta/main.yml new file mode 100644 index 0000000..236e381 --- /dev/null +++ b/roles/sigvild-gallery/meta/main.yml @@ -0,0 +1,20 @@ +--- +# Role Dependencies +dependencies: + - role: caddy + +galaxy_info: + role_name: sigvild-gallery + author: "Rick Infrastructure Team" + description: "Deploys Sigvild Wedding Gallery with PocketBase API and SvelteKit frontend" + company: "" + license: "license (MIT)" + min_ansible_version: "2.9" + + platforms: + - name: Archlinux + versions: + - all + - name: Ubuntu + versions: + - all diff --git a/roles/sigvild-gallery/tasks/deploy_backend.yml b/roles/sigvild-gallery/tasks/deploy_backend.yml new file mode 100644 index 0000000..c587b77 --- /dev/null +++ b/roles/sigvild-gallery/tasks/deploy_backend.yml @@ -0,0 +1,43 @@ +--- +# Backend Deployment Tasks + +- name: Build Go binary locally + local_action: + module: shell + cmd: GOOS=linux GOARCH=amd64 go build -o sigvild-gallery . + chdir: "{{ sigvild_gallery_local_project_path }}" + become: no + tags: [backend, build] + +- name: Check if binary was built successfully + local_action: + module: stat + path: "{{ sigvild_gallery_local_project_path }}/sigvild-gallery" + register: binary_stat + become: no + tags: [backend, build] + +- name: Fail if binary doesn't exist + fail: + msg: "Failed to build sigvild-gallery binary" + when: not binary_stat.stat.exists + tags: [backend, build] + +- name: Transfer Go binary + copy: + src: "{{ sigvild_gallery_local_project_path }}/sigvild-gallery" + dest: "{{ sigvild_gallery_binary }}" + owner: "{{ sigvild_gallery_user }}" + group: "{{ sigvild_gallery_user }}" + mode: '0755' + notify: restart sigvild-gallery + tags: [backend] + +- name: Create data directory for PocketBase + file: + path: "{{ sigvild_gallery_data_dir }}" + state: directory + owner: "{{ sigvild_gallery_user }}" + group: "{{ sigvild_gallery_user }}" + mode: '0755' + tags: [backend] diff --git a/roles/sigvild-gallery/tasks/deploy_frontend.yml b/roles/sigvild-gallery/tasks/deploy_frontend.yml new file mode 100644 index 0000000..594744e --- /dev/null +++ b/roles/sigvild-gallery/tasks/deploy_frontend.yml @@ -0,0 +1,57 @@ +--- +# Frontend Deployment Tasks + +- name: Check if frontend source exists + local_action: + module: stat + path: "{{ sigvild_gallery_local_project_path }}/sigvild-kit" + register: frontend_source + become: no + tags: [frontend, build] + +- name: Fail if frontend source doesn't exist + fail: + msg: "Frontend source directory not found at {{ sigvild_gallery_local_project_path }}/sigvild-kit" + when: not frontend_source.stat.exists + tags: [frontend, build] + +- name: Install frontend dependencies + local_action: + module: shell + cmd: npm install + chdir: "{{ sigvild_gallery_local_project_path }}/sigvild-kit" + become: no + tags: [frontend, build] + +- name: Build frontend for production + local_action: + module: shell + cmd: npm run build:production + chdir: "{{ sigvild_gallery_local_project_path }}/sigvild-kit" + become: no + tags: [frontend, build] + +- name: Check if frontend build exists + local_action: + module: stat + path: "{{ sigvild_gallery_local_project_path }}/sigvild-kit/build" + register: frontend_build + become: no + tags: [frontend, build] + +- name: Fail if frontend build doesn't exist + fail: + msg: "Frontend build failed - build directory not found" + when: not frontend_build.stat.exists + become: no + tags: [frontend, build] + +- name: Sync frontend files to web root + synchronize: + src: "{{ sigvild_gallery_local_project_path }}/sigvild-kit/build/" + dest: "{{ sigvild_gallery_web_root }}/" + delete: yes + rsync_opts: + - "--exclude=.git" + - "--chown={{ sigvild_gallery_user }}:{{ sigvild_gallery_user }}" + tags: [frontend] diff --git a/roles/sigvild-gallery/tasks/main.yml b/roles/sigvild-gallery/tasks/main.yml new file mode 100644 index 0000000..46f847c --- /dev/null +++ b/roles/sigvild-gallery/tasks/main.yml @@ -0,0 +1,91 @@ +--- +# Sigvild Gallery Deployment Tasks + +- name: Install required packages + pacman: + name: + - rsync + state: present +- name: Create sigvild gallery user + user: + name: "{{ sigvild_gallery_user }}" + system: yes + shell: /bin/bash + home: "{{ sigvild_gallery_home }}" + create_home: yes + +- name: Create directories + file: + path: "{{ item }}" + state: directory + owner: "{{ sigvild_gallery_user }}" + group: "{{ sigvild_gallery_user }}" + mode: '0755' + loop: + - "{{ sigvild_gallery_home }}" + - "{{ sigvild_gallery_data_dir }}" + - "{{ sigvild_gallery_web_root }}" + +- name: Build and deploy backend + include_tasks: deploy_backend.yml + tags: [backend, build] + +- name: Build and deploy frontend + include_tasks: deploy_frontend.yml + tags: [frontend, build] + +- name: Deploy systemd service + template: + src: sigvild-gallery.service.j2 + dest: /etc/systemd/system/sigvild-gallery.service + owner: root + group: root + mode: '0644' + notify: + - reload systemd + - restart sigvild-gallery + tags: [backend, service] + +- name: Deploy Caddy configurations + template: + src: "{{ item.src }}" + dest: "{{ caddy_sites_enabled_dir }}/{{ item.dest }}" + owner: root + group: "{{ caddy_user }}" + mode: '0644' + loop: + - { src: 'frontend.caddy.j2', dest: 'sigvild-frontend.caddy' } + - { src: 'api.caddy.j2', dest: 'sigvild-api.caddy' } + notify: reload caddy + tags: [caddy, frontend, backend] + +- name: Enable and start sigvild-gallery service + systemd: + name: sigvild-gallery + enabled: "{{ sigvild_gallery_service_enabled }}" + state: "{{ sigvild_gallery_service_state }}" + daemon_reload: yes + tags: [backend, service] + +- name: Create superuser account + command: > + {{ sigvild_gallery_binary }} superuser upsert + "{{ vault_pb_su_email }}" + "{{ vault_pb_su_password }}" + args: + chdir: "{{ sigvild_gallery_home }}" + become: yes + become_user: "{{ sigvild_gallery_user }}" + register: superuser_result + failed_when: superuser_result.rc != 0 + +- name: Verify gallery health + uri: + url: "https://{{ sigvild_gallery_api_domain }}/api/health" + method: GET + status_code: [200, 404] # 404 is ok if health endpoint doesn't exist yet + timeout: 15 + retries: 5 + delay: 5 + ignore_errors: yes + tags: [verify] diff --git a/roles/sigvild-gallery/templates/api.caddy.j2 b/roles/sigvild-gallery/templates/api.caddy.j2 new file mode 100644 index 0000000..8feb49f --- /dev/null +++ b/roles/sigvild-gallery/templates/api.caddy.j2 @@ -0,0 +1,45 @@ +{{ sigvild_gallery_api_domain }} { + reverse_proxy {{ sigvild_gallery_host }}:{{ sigvild_gallery_port }} { + header_up Host {upstream_hostport} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-Proto https + + # Health check + health_uri /api/health + health_timeout 5s + health_interval 30s + } + + # CORS headers for frontend domain + @cors { + header Origin https://{{ sigvild_gallery_frontend_domain }} + } + header @cors { + Access-Control-Allow-Origin "https://{{ sigvild_gallery_frontend_domain }}" + Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS" + Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" + Access-Control-Allow-Credentials true + Access-Control-Max-Age 86400 + } + + # Handle preflight requests + @preflight { + method OPTIONS + } + respond @preflight 204 + + # Security headers for API + header { + X-Frame-Options DENY + X-Content-Type-Options nosniff + X-XSS-Protection "1; mode=block" + Referrer-Policy strict-origin-when-cross-origin + } + + # API logging + log { + output file /var/log/caddy/sigvild-api.log + level INFO + format json + } +} diff --git a/roles/sigvild-gallery/templates/frontend.caddy.j2 b/roles/sigvild-gallery/templates/frontend.caddy.j2 new file mode 100644 index 0000000..f04e282 --- /dev/null +++ b/roles/sigvild-gallery/templates/frontend.caddy.j2 @@ -0,0 +1,42 @@ +{{ sigvild_gallery_frontend_domain }} { + root * {{ sigvild_gallery_web_root }} + file_server + + # SPA routing - serve index.html for all routes + try_files {path} /index.html + + # Security headers + header { + X-Frame-Options DENY + 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 + } + 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 for debugging (can be removed in production) + log { + output file /var/log/caddy/sigvild-frontend.log + level INFO + } +} \ No newline at end of file diff --git a/roles/sigvild-gallery/templates/sigvild-gallery.service.j2 b/roles/sigvild-gallery/templates/sigvild-gallery.service.j2 new file mode 100644 index 0000000..b93be3e --- /dev/null +++ b/roles/sigvild-gallery/templates/sigvild-gallery.service.j2 @@ -0,0 +1,36 @@ +[Unit] +Description=Sigvild Wedding Gallery API +After=network.target + +[Service] +Type=simple +User={{ sigvild_gallery_user }} +Group={{ sigvild_gallery_user }} +WorkingDirectory={{ sigvild_gallery_home }} +ExecStart={{ sigvild_gallery_binary }} serve --http={{ sigvild_gallery_host }}:{{ sigvild_gallery_port }} + +# Environment variables +Environment="SIGVILD_ENVIRONMENT"="production" # Lets caddy handle CORS +Environment="HOST_USERNAME={{ sigvild_gallery_host_username }}" +Environment="HOST_PASSWORD={{ sigvild_gallery_host_password }}" +Environment="HOST_DISPLAY_NAME=Wedding Host" +Environment="GUEST_USERNAME={{ sigvild_gallery_guest_username }}" +Environment="GUEST_PASSWORD={{ sigvild_gallery_guest_password }}" +Environment="GUEST_DISPLAY_NAME=Wedding Guest" + +# Restart configuration +Restart=always +RestartSec=3 + +# Security sandboxing +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +ReadWritePaths={{ sigvild_gallery_data_dir }} + +# Allow binding to port (if needed) +AmbientCapabilities=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target diff --git a/site.yml b/site.yml index 28ad20d..793ff6b 100644 --- a/site.yml +++ b/site.yml @@ -2,7 +2,7 @@ # Core infrastructure deployment with security hardening first # Security hardening establishes secure foundation before web services -- import_playbook: playbooks/security.yml +# - import_playbook: playbooks/security.yml - name: Deploy Core Infrastructure hosts: arch-vps @@ -10,8 +10,10 @@ gather_facts: yes roles: - - role: caddy - tags: ['caddy', 'infrastructure', 'web'] + # - role: caddy + # tags: ['caddy', 'infrastructure', 'web'] + - role: sigvild-gallery + tags: ['sigvild', 'gallery', 'wedding'] post_tasks: - name: Verify Caddy API is accessible