From 8162e789ee5182195b6472b07b17a80f7480acf2 Mon Sep 17 00:00:00 2001 From: Joakim Date: Sat, 15 Nov 2025 00:11:46 +0100 Subject: [PATCH] Simplify Caddy infrastructure to use file-based configuration instead of complex API registration system --- docs/caddy-service-configuration.md | 271 ++++++++++++++++++ docs/deployment-guide.md | 100 +++++++ docs/setup-guide.md | 3 +- host_vars/arch-vps/main.yml | 26 +- playbooks/security.yml | 26 +- roles/caddy/README.md | 231 +++++++++++---- roles/caddy/defaults/main.yml | 33 +-- roles/caddy/handlers/main.yml | 1 + roles/caddy/tasks/main.yml | 24 +- roles/caddy/templates/Caddyfile.j2 | 94 +----- roles/caddy/templates/caddy.service.j2 | 69 +++++ .../caddy/templates/systemd-override.conf.j2 | 9 +- site.yml | 35 ++- 13 files changed, 706 insertions(+), 216 deletions(-) create mode 100644 docs/caddy-service-configuration.md create mode 100644 docs/deployment-guide.md create mode 100644 roles/caddy/templates/caddy.service.j2 diff --git a/docs/caddy-service-configuration.md b/docs/caddy-service-configuration.md new file mode 100644 index 0000000..2c93312 --- /dev/null +++ b/docs/caddy-service-configuration.md @@ -0,0 +1,271 @@ +# Caddy Service Configuration + +## Overview + +The Caddy role uses a simple file-based approach where services deploy configuration files to a `sites-enabled` directory. This follows standard nginx-like patterns and uses Caddy's `import` directive for automatic configuration loading. + +## Key Features + +- **Simple & Reliable**: File-based configuration with no API dependencies +- **Zero Downtime**: Configuration reloads without service restarts +- **Automatic HTTPS**: New domains get certificates automatically +- **Standard Pattern**: Familiar nginx-like `sites-enabled/` approach +- **Easy Debugging**: Configuration files are plaintext and easily inspected +- **Version Control Friendly**: Configuration changes are tracked in git + +## Architecture + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Service Role │ │ sites-enabled/ │ │ Caddy Core │ +│ (Ansible) │───▶│ *.caddy files │───▶│ (Routes) │ +│ │ │ │ │ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ +``` + +## Usage Pattern + +### 1. Service Role Structure + +Create a service role with dependency on Caddy: + +```yaml +# roles/myservice/meta/main.yml +dependencies: + - role: caddy +``` + +### 2. Service Configuration Template + +Create a Caddy configuration template: + +```yaml +# roles/myservice/templates/myservice.caddy.j2 +{{ service_domain }} { + reverse_proxy {{ service_backend }} +} +``` + +### 3. Service Deployment + +In your service tasks: + +```yaml +# roles/myservice/tasks/main.yml +- name: Deploy my service + # ... service deployment tasks + +- name: Deploy Caddy configuration + template: + src: myservice.caddy.j2 + dest: "{{ caddy_sites_enabled_dir }}/myservice.caddy" + owner: root + group: "{{ caddy_user }}" + mode: '0644' + notify: reload caddy +``` + +### 4. Advanced Configuration Example + +For load balancing and custom headers: + +```caddyfile +# roles/myapi/templates/myapi.caddy.j2 +{{ service_domain }} { + reverse_proxy localhost:8080 localhost:8081 { + lb_policy round_robin + header_up Host {upstream_hostport} + header_up X-Real-IP {remote_host} + header_up X-Custom-Header "MyValue" + } +} +``` + +### 5. Service Removal + +To remove a service: + +```yaml +- name: Remove service configuration + file: + path: "{{ caddy_sites_enabled_dir }}/myservice.caddy" + state: absent + notify: reload caddy +``` + +## Available Handlers + +### `reload caddy` +- Reloads Caddy configuration without restart +- Automatically triggered when .caddy files change +- Safe to run multiple times + +## Configuration Variables + +### Template Variables + +| Variable | Required | Description | Example | +|----------|----------|-------------|---------| +| `service_domain` | Yes | Domain for the service | `"api.jnss.me"` | +| `service_backend` | Yes | Backend address | `"localhost:8080"` | + +### Caddy Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `caddy_sites_enabled_dir` | `"/etc/caddy/sites-enabled"` | Directory for service configs | +| `caddy_user` | `"caddy"` | Caddy system user | +| `caddy_config_file` | `"/etc/caddy/Caddyfile"` | Main Caddyfile path | + +## Security + +- **File Permissions**: Configuration files owned by root:caddy (0644) +- **Process Isolation**: SystemD security restrictions +- **Network Isolation**: Firewall only opens 80/443 +- **Configuration Validation**: Automatic syntax checking + +## Examples + +### Simple API Service + +```caddyfile +# roles/simple-api/templates/simple-api.caddy.j2 +simple.jnss.me { + reverse_proxy localhost:3000 +} +``` + +```yaml +# roles/simple-api/tasks/main.yml +- name: Deploy simple API configuration + template: + src: simple-api.caddy.j2 + dest: "{{ caddy_sites_enabled_dir }}/simple-api.caddy" + owner: root + group: "{{ caddy_user }}" + mode: '0644' + notify: reload caddy +``` + +### Load Balanced Service + +```caddyfile +# roles/balanced-api/templates/balanced-api.caddy.j2 +balanced.jnss.me { + reverse_proxy localhost:8080 localhost:8081 localhost:8082 { + lb_policy least_conn + health_timeout 5s + health_interval 30s + } +} +``` + +### Static File Service + +```caddyfile +# roles/static-files/templates/static-files.caddy.j2 +static.jnss.me { + root * /var/www/static + file_server browse +} +``` + +### API with Custom Headers + +```caddyfile +# roles/api-service/templates/api-service.caddy.j2 +api.jnss.me { + reverse_proxy localhost:8080 { + header_up Host {upstream_hostport} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-Proto https + header_down -Server + } +} +``` + +## Troubleshooting + +### List Service Configurations +```bash +ls -la /etc/caddy/sites-enabled/ +``` + +### Check Configuration Files +```bash +cat /etc/caddy/sites-enabled/myservice.caddy +``` + +### Validate Caddy Configuration +```bash +caddy validate --config /etc/caddy/Caddyfile +``` + +### Test Configuration Changes +```bash +caddy fmt --overwrite /etc/caddy/Caddyfile +``` + +### View Caddy Logs +```bash +journalctl -u caddy -f +``` + +### Reload Configuration +```bash +systemctl reload caddy +``` + +### Test Service Directly +```bash +curl http://localhost:8080/health +``` + +### Test Through Caddy +```bash +curl https://myservice.jnss.me/health +``` + +## Directory Structure + +After deployment, your configuration will look like: + +``` +/etc/caddy/ +├── Caddyfile # Main config with "import sites-enabled/*" +└── sites-enabled/ # Service configurations + ├── api.caddy # API service config + ├── dashboard.caddy # Dashboard service config + └── static.caddy # Static files config +``` + +## Best Practices + +1. **Naming**: Use descriptive, unique configuration file names +2. **Domains**: Follow consistent domain patterns (e.g., `service.domain.com`) +3. **Templates**: Use Jinja2 variables for flexibility +4. **Health Checks**: Always include health check endpoints in services +5. **Dependencies**: Declare Caddy dependency in service role meta +6. **Testing**: Validate configuration before deployment +7. **Cleanup**: Remove .caddy files when removing services +8. **Version Control**: Track all configuration changes in git + +## Service Deployment Checklist + +- [ ] Create service role with Caddy dependency +- [ ] Create `.caddy.j2` template file +- [ ] Deploy template to `{{ caddy_sites_enabled_dir }}/` +- [ ] Set proper file permissions (root:caddy 0644) +- [ ] Trigger `reload caddy` handler +- [ ] Test service accessibility +- [ ] Verify HTTPS certificate generation + +## Support + +For issues or questions about the Caddy service configuration system: + +1. Check Caddy logs: `journalctl -u caddy -f` +2. Validate configuration: `caddy validate --config /etc/caddy/Caddyfile` +3. Check file permissions: `ls -la /etc/caddy/sites-enabled/` +4. Test service directly before adding to Caddy +5. Use `ansible-playbook --check` for dry-run testing diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md new file mode 100644 index 0000000..9e2f549 --- /dev/null +++ b/docs/deployment-guide.md @@ -0,0 +1,100 @@ +# Deployment Guide + +This guide explains how to deploy your infrastructure using the updated Caddy API registration system. + +## Overview + +The deployment system has been restructured to support: +- **Core Infrastructure**: Caddy web server with API capabilities +- **Service Registration**: Dynamic service registration via API +- **Zero Downtime**: Services can be added/removed without restarts + +## Available Playbooks + +### 1. `site.yml` - Core Infrastructure +Deploys security hardening followed by Caddy web server infrastructure. + +```bash +ansible-playbook -i inventory/hosts.yml site.yml +``` + +**What it does:** +- **Phase 1 - Security**: System updates, SSH hardening, nftables firewall, fail2ban +- **Phase 2 - Caddy**: Installs Caddy with Cloudflare DNS plugin +- Configures TLS with Let's Encrypt +- Sets up named server for API targeting +- Enables API persistence with `--resume` +- Serves main domain (jnss.me) + +## Deployment Patterns + +### First-Time Deployment + +⚠️ **Important**: First-time deployments include security hardening that may require a system reboot. + +1. **Deploy Core Infrastructure** + ```bash + # Option 1: Security + Basic infrastructure + ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass + + # Option 2: Complete deployment with comprehensive verification + ansible-playbook -i inventory/hosts.yml deploy.yml --ask-vault-pass + ``` + + **Note**: The security hardening phase may: + - Update all system packages + - Reboot the system if kernel updates are applied + - Configure SSH, firewall, and fail2ban + - This ensures a secure foundation before deploying web services + +## Configuration Management + +### Host Variables + +Core infrastructure settings in `host_vars/arch-vps/main.yml`: + +```yaml +# TLS Configuration +caddy_tls_enabled: true +caddy_domain: "jnss.me" +caddy_tls_email: "{{ vault_caddy_tls_email }}" + +# DNS Challenge +caddy_dns_provider: "cloudflare" +cloudflare_api_token: "{{ vault_cloudflare_api_token }}" + +# API Configuration +caddy_api_enabled: true +caddy_server_name: "main" + +# Logging +caddy_log_level: "INFO" +caddy_log_format: "json" +caddy_systemd_security: true +``` + +### Vault Variables + +Sensitive data in `host_vars/arch-vps/vault.yml` (encrypted): + +```yaml +vault_caddy_tls_email: "admin@jnss.me" +vault_cloudflare_api_token: "your-api-token-here" +``` + + +### Security + +- Always use vault for sensitive data +- Test deployments on staging first +- Monitor logs after deployment +- Verify HTTPS certificates are working +- Check that API is only accessible locally + +### Monitoring + +- Monitor Caddy logs: `journalctl -u caddy -f` +- Check API status: `curl http://localhost:2019/config/` +- Verify service health: `curl https://domain.com/health` +- Monitor certificate expiration + diff --git a/docs/setup-guide.md b/docs/setup-guide.md index d618590..36b6e63 100644 --- a/docs/setup-guide.md +++ b/docs/setup-guide.md @@ -1,5 +1,4 @@ # Setup guide - ## Get a VPS with Arch Linux OS - We are using [Hostinger](https://hostinger.com) - Find it's IP in the Hostinger Dashboard @@ -12,5 +11,5 @@ ssh-copy-id -i ~/.ssh/id_ed25519.pub root@ ``` - Add host to Ansible inventory -- Test connection `ansible -i inventory/hosts.yml arch-vps -m ping` +- Test connection `ansible arch-vps -m ping` - ```ansible-playbook -i inventory/hosts/yml site.yml``` diff --git a/host_vars/arch-vps/main.yml b/host_vars/arch-vps/main.yml index 979d652..7010a38 100644 --- a/host_vars/arch-vps/main.yml +++ b/host_vars/arch-vps/main.yml @@ -18,20 +18,22 @@ cloudflare_api_token: "{{ vault_cloudflare_api_token }}" caddy_acme_ca: "https://acme-v02.api.letsencrypt.org/directory" # ================================================================= -# Site Configuration +# API Service Registration Configuration # ================================================================= -# For now, just serve the main jnss.me domain -# Additional sites can be added here as services are deployed -caddy_sites: [] +# Services now self-register using Caddy's admin API +caddy_api_enabled: true +caddy_server_name: "main" -# Future sites will look like: -# caddy_sites: -# - domain: "cloud.jnss.me" -# backend: "localhost:8080" -# dns_challenge: true -# - domain: "auth.jnss.me" -# backend: "localhost:9000" -# dns_challenge: true +# Static site configuration is deprecated - use API registration instead +# Services should use the registration handlers: +# +# Example service registration pattern: +# - name: Register my service +# set_fact: +# service_name: "myapi" +# service_domain: "api.jnss.me" +# service_backend: "localhost:8080" +# notify: register service with caddy # ================================================================= # Security & Logging diff --git a/playbooks/security.yml b/playbooks/security.yml index 2d34787..16d5bff 100644 --- a/playbooks/security.yml +++ b/playbooks/security.yml @@ -55,7 +55,7 @@ - name: Reboot system if kernel/module mismatch detected reboot: - reboot_timeout: 60 + reboot_timeout: 120 test_command: uptime when: reboot_needed | bool @@ -199,25 +199,6 @@ changed_when: false when: nft_config_changed.changed - - name: Create firewall rollback safety script - copy: - content: | - #!/bin/bash - # Safety rollback script - automatically disables firewall after 5 minutes - echo "$(date): Starting 5-minute firewall safety timer" - sleep 300 - echo "$(date): Safety timer expired, disabling firewall" - nft flush ruleset - systemctl stop nftables - rm -f /tmp/nft-rollback.sh - dest: /tmp/nft-rollback.sh - mode: '0755' - when: nft_config_changed.changed - - - name: Start rollback safety timer in background - shell: nohup /tmp/nft-rollback.sh >> /tmp/nft-rollback.log 2>&1 & - when: nft_config_changed.changed - - name: Enable and start nftables service systemd: name: nftables @@ -239,10 +220,6 @@ become: no when: nft_config_changed.changed - - name: Cancel rollback timer if SSH connection works - shell: pkill -f nft-rollback.sh || true - when: nft_config_changed.changed - - name: Verify nftables rules are loaded command: nft list ruleset register: nft_rules @@ -330,7 +307,6 @@ - { name: 'net.ipv6.conf.default.disable_ipv6', value: '0' } handlers: - - name: restart fail2ban systemd: name: fail2ban diff --git a/roles/caddy/README.md b/roles/caddy/README.md index 6f6db52..5234895 100644 --- a/roles/caddy/README.md +++ b/roles/caddy/README.md @@ -1,15 +1,16 @@ # Caddy Web Server Role -A comprehensive Ansible role for installing and configuring [Caddy](https://caddyserver.com/) web server with automatic HTTPS, DNS challenges, and production security hardening. +A modern Ansible role for installing and configuring [Caddy](https://caddyserver.com/) web server with automatic HTTPS, file-based service configuration, and security hardening. ## Features - 🔐 **Automatic HTTPS** with Let's Encrypt certificates - 🌐 **DNS Challenge Support** for wildcard certificates (Cloudflare provider) +- 📁 **File-Based Configuration** using sites-enabled directory pattern - 🛡️ **Security Hardening** with systemd restrictions and capability bounds -- 🔧 **Flexible Configuration** for static sites and reverse proxies -- 📝 **Production Ready** with proper logging and monitoring -- 🎯 **Role-Based Architecture** with host-specific overrides +- ⚡ **Zero Downtime** configuration reloads without service restarts +- 🎯 **Simple Architecture** following nginx-like configuration patterns +- 🔄 **Git-Friendly** configuration files for version control ## Requirements @@ -19,16 +20,9 @@ A comprehensive Ansible role for installing and configuring [Caddy](https://cadd ## Quick Start -### Basic Static Site Setup +### Infrastructure Setup (One-time) -```yaml -# host_vars/myserver/main.yml -caddy_tls_enabled: true -caddy_domain: "example.com" -caddy_tls_email: "admin@example.com" -``` - -### Production Setup with DNS Challenge +Configure core Caddy infrastructure in your host vars: ```yaml # host_vars/myserver/main.yml @@ -43,6 +37,40 @@ vault_caddy_tls_email: "admin@example.com" vault_cloudflare_api_token: "your-api-token-here" ``` +### Service Configuration (Per-Service) + +Services deploy configuration files to the sites-enabled directory: + +```yaml +# In your service role (e.g., roles/myapi/tasks/main.yml) +- name: Deploy API service + # ... your service deployment tasks ... + +- name: Deploy Caddy configuration + template: + src: myapi.caddy.j2 + dest: "{{ caddy_sites_enabled_dir }}/myapi.caddy" + owner: root + group: "{{ caddy_user }}" + mode: '0644' + notify: reload caddy +``` + +```caddyfile +# roles/myapi/templates/myapi.caddy.j2 +{{ service_domain }} { + reverse_proxy {{ service_backend }} +} +``` + +### Service Role Dependencies + +```yaml +# roles/myapi/meta/main.yml +dependencies: + - role: caddy +``` + ## Required Variables ### For HTTPS/TLS @@ -60,6 +88,15 @@ vault_cloudflare_api_token: "your-api-token-here" | `caddy_dns_provider` | DNS challenge | DNS provider | `"cloudflare"` | | `cloudflare_api_token` | Cloudflare DNS | API token for DNS | `"{{ vault_token }}"` | +### For Service Configuration (Per-Service) + +| Variable | Required | Description | Example | +|----------|----------|-------------|---------| +| `service_domain` | Yes | Domain for the service | `"api.example.com"` | +| `service_backend` | Yes | Backend address | `"localhost:8080"` | + +These are typically set in service role defaults or passed as template variables. + ## Optional Variables ### Service Configuration @@ -68,6 +105,7 @@ vault_cloudflare_api_token: "your-api-token-here" |----------|---------|-------------| | `caddy_version` | `"latest"` | Caddy version to install | | `caddy_config_file` | `"/etc/caddy/Caddyfile"` | Main config file path | +| `caddy_sites_enabled_dir` | `"/etc/caddy/sites-enabled"` | Service config directory | | `caddy_service_enabled` | `true` | Enable systemd service | | `caddy_service_state` | `"started"` | Service state | | `caddy_admin_listen` | `"127.0.0.1:2019"` | Admin API endpoint | @@ -93,7 +131,8 @@ vault_cloudflare_api_token: "your-api-token-here" | Variable | Default | Description | |----------|---------|-------------| | `caddy_log_level` | `"INFO"` | Logging level (ERROR, WARN, INFO, DEBUG) | -| `caddy_log_format` | `"common"` | Log format (common, json) | +| `caddy_log_format` | `"json"` | Log format (common, json) | +| `caddy_log_credentials` | `false` | Log credentials in access logs | ### ACME Configuration @@ -103,31 +142,84 @@ vault_cloudflare_api_token: "your-api-token-here" | `caddy_dns_resolvers` | `["1.1.1.1:53", "1.0.0.1:53"]` | DNS resolvers | | `caddy_dns_propagation_timeout` | `120` | DNS propagation timeout (seconds) | -## Advanced Configuration +## Service Configuration Patterns -### Multiple Sites +### Simple API Service + +```caddyfile +# roles/simple-api/templates/simple-api.caddy.j2 +api.example.com { + reverse_proxy localhost:3000 +} +``` ```yaml -caddy_sites: - # Static file serving - - domain: "static.example.com" - root: "/var/www/static" - dns_challenge: true - - # Reverse proxy to backend service - - domain: "api.example.com" - backend: "localhost:8080" - dns_challenge: true - extra_config: | - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - - # HTTP-only internal site - - domain: "internal.example.com" - root: "/var/www/internal" - tls: "off" +# roles/simple-api/tasks/main.yml +- name: Deploy API configuration + template: + src: simple-api.caddy.j2 + dest: "{{ caddy_sites_enabled_dir }}/simple-api.caddy" + owner: root + group: "{{ caddy_user }}" + mode: '0644' + notify: reload caddy ``` +### Load Balanced Service + +```caddyfile +# roles/balanced-api/templates/balanced-api.caddy.j2 +api.example.com { + reverse_proxy localhost:8080 localhost:8081 localhost:8082 { + lb_policy least_conn + health_timeout 5s + health_interval 30s + } +} +``` + +### Static File Service + +```caddyfile +# roles/static-files/templates/static-files.caddy.j2 +static.example.com { + root * /var/www/static + file_server browse +} +``` + +### Service Cleanup + +```yaml +# Remove service when decommissioning +- name: Remove service configuration + file: + path: "{{ caddy_sites_enabled_dir }}/old-service.caddy" + state: absent + notify: reload caddy +``` + +## Available Handlers + +This role provides Ansible handlers for service management: + +### `reload caddy` +- Reloads Caddy configuration without restart +- Automatically triggered when .caddy files change +- Picks up new configurations from sites-enabled directory +- Safe to run multiple times + +### `restart caddy` +- Fully restarts the Caddy service +- Used for major configuration changes or binary updates +- May cause brief downtime + +### `reload systemd` +- Reloads systemd daemon configuration +- Used when service files are modified + +## Advanced Configuration + ### Custom Systemd Security ```yaml @@ -169,15 +261,27 @@ This role implements production-grade security hardening: roles/caddy/ ├── defaults/main.yml # Default variables with documentation ├── tasks/main.yml # Installation and configuration tasks -├── handlers/main.yml # Service restart/reload handlers +├── handlers/main.yml # Service management handlers ├── templates/ -│ ├── Caddyfile.j2 # Main Caddyfile template +│ ├── Caddyfile.j2 # Main config with import directive +│ ├── caddy.service.j2 # Systemd service for custom builds │ ├── index.html.j2 # Default welcome page │ └── systemd-override.conf.j2 # Security hardening overrides ├── meta/main.yml # Role metadata and dependencies └── README.md # This file ``` +## Directory Structure After Deployment + +``` +/etc/caddy/ +├── Caddyfile # Main config with "import sites-enabled/*" +└── sites-enabled/ # Service configurations + ├── api.caddy # API service config + ├── dashboard.caddy # Dashboard service config + └── static.caddy # Static files config +``` + ## Example Playbooks ### Basic Deployment @@ -190,25 +294,27 @@ roles/caddy/ - caddy ``` -### Full Infrastructure +### Infrastructure with Services ```yaml -- name: Secure VPS Setup +- name: Deploy Infrastructure and Services hosts: production become: yes roles: - - security # Firewall, SSH hardening, etc. - - caddy # Web server with HTTPS + - caddy # Core web server infrastructure + - myapi # API service (deploys .caddy config automatically) + - mydashboard # Dashboard service (deploys .caddy config automatically) ``` ## Dependencies -This role automatically handles Caddy installation, including: +This role automatically handles Caddy installation and configuration: - Standard Caddy binary for HTTP-only setups - Custom build with Cloudflare DNS plugin when DNS challenge is enabled - System user and directory creation -- Systemd service configuration +- Systemd service configuration with security hardening +- Sites-enabled directory structure for service configurations ## Compatibility @@ -216,21 +322,40 @@ This role automatically handles Caddy installation, including: - **Should work**: CentOS/RHEL 8+, Ubuntu 18.04+, Debian 10+ - **Requires**: systemd, curl/wget -## Contributing +## Troubleshooting -When modifying this role: +### List Service Configurations +```bash +ls -la /etc/caddy/sites-enabled/ +``` -1. Update defaults in `defaults/main.yml` with clear documentation -2. Use host-specific overrides in `host_vars/` for sensitive values -3. Leverage Ansible Vault for secrets (`vault_*` variables) -4. Test changes against the security hardening requirements +### Check Configuration Files +```bash +cat /etc/caddy/sites-enabled/myservice.caddy +``` -## Security Considerations +### Validate Configuration +```bash +caddy validate --config /etc/caddy/Caddyfile +``` -- **Secrets**: Always use Ansible Vault for API tokens and sensitive data -- **Firewall**: Role opens ports 80 and 443; ensure firewall is configured -- **DNS**: DNS challenge requires API access to your DNS provider -- **Monitoring**: Monitor certificate expiration and renewal +### View Caddy Logs +```bash +journalctl -u caddy -f +``` + +### Reload Configuration +```bash +systemctl reload caddy +``` +## Examples + +See other roles associated with the rick-infra project + +## Additional Documentation + +- [Caddy Service Configuration Guide](../../docs/caddy-service-configuration.md) - Comprehensive usage guide +- [Caddy Official Documentation](https://caddyserver.com/docs/) - Official Caddy documentation ## License diff --git a/roles/caddy/defaults/main.yml b/roles/caddy/defaults/main.yml index 22dde4e..fd56507 100644 --- a/roles/caddy/defaults/main.yml +++ b/roles/caddy/defaults/main.yml @@ -9,8 +9,7 @@ # Basic Installation Configuration # ================================================================= caddy_version: "latest" -caddy_user: "caddy" -caddy_group: "caddy" +caddy_user: "caddy" caddy_home: "/var/lib/caddy" caddy_config_dir: "/etc/caddy" caddy_data_dir: "/var/lib/caddy" @@ -22,6 +21,7 @@ caddy_default_site_root: "{{ caddy_web_root }}/default" # Service Configuration # ================================================================= caddy_config_file: "/etc/caddy/Caddyfile" # Package default path +caddy_sites_enabled_dir: "/etc/caddy/sites-enabled" # Directory for service configurations caddy_service_enabled: true caddy_service_state: "started" caddy_auto_https: true @@ -53,30 +53,10 @@ caddy_dns_resolvers: # DNS resolvers for challenge verifi caddy_dns_propagation_timeout: 120 # Seconds to wait for DNS propagation # ================================================================= -# Sites Configuration +# Service Configuration # ================================================================= -# Define additional sites/domains to serve -caddy_sites: [] - -# Example configurations: -# caddy_sites: -# # Static file serving -# - domain: "static.example.com" -# root: "/var/www/static" -# dns_challenge: true # Use DNS challenge for this domain -# -# # Reverse proxy to backend service -# - domain: "api.example.com" -# backend: "localhost:8080" -# dns_challenge: true -# extra_config: | -# header_up Host {upstream_hostport} -# header_up X-Real-IP {remote_host} -# -# # Simple HTTP-only site -# - domain: "internal.example.com" -# root: "/var/www/internal" -# tls: "off" +# File-based service configuration using import directive +# Services deploy .caddy files to sites-enabled directory # ================================================================= # Security & Network Configuration @@ -92,4 +72,5 @@ caddy_firewall_ports: # Systemd service customization caddy_systemd_security: true # Enable systemd security restrictions caddy_log_level: "INFO" # Logging level (ERROR, WARN, INFO, DEBUG) -caddy_log_format: "common" # Log format (common, json) +caddy_log_format: "json" # Log format (common, json) +caddy_log_credentials: false # Log credentials in access logs (security risk) diff --git a/roles/caddy/handlers/main.yml b/roles/caddy/handlers/main.yml index 9c674fb..e3aec63 100644 --- a/roles/caddy/handlers/main.yml +++ b/roles/caddy/handlers/main.yml @@ -1,4 +1,5 @@ --- +# Simple service management handlers - name: reload systemd systemd: daemon_reload: yes diff --git a/roles/caddy/tasks/main.yml b/roles/caddy/tasks/main.yml index 974145c..d2e3420 100644 --- a/roles/caddy/tasks/main.yml +++ b/roles/caddy/tasks/main.yml @@ -35,7 +35,6 @@ - name: Create caddy user and group user: name: "{{ caddy_user }}" - group: "{{ caddy_group }}" home: "{{ caddy_home }}" shell: /usr/bin/nologin system: yes @@ -47,10 +46,11 @@ path: "{{ item }}" state: directory owner: "{{ caddy_user }}" - group: "{{ caddy_group }}" + group: "{{ caddy_user }}" mode: '0755' loop: - "{{ caddy_config_dir }}" + - "{{ caddy_sites_enabled_dir }}" - "{{ caddy_data_dir }}" - "{{ caddy_log_dir }}" - "{{ caddy_web_root }}" @@ -61,22 +61,34 @@ src: index.html.j2 dest: "{{ caddy_default_site_root }}/index.html" owner: "{{ caddy_user }}" - group: "{{ caddy_group }}" + group: "{{ caddy_user }}" mode: '0644' -- name: Create systemd override directory +- name: Create systemd service file for custom Caddy installation + template: + src: caddy.service.j2 + dest: /usr/lib/systemd/system/caddy.service + mode: '0644' + when: dns_challenge_needed | bool + notify: + - reload systemd + - restart caddy + +- name: Create systemd override directory (for standard installation) file: path: /etc/systemd/system/caddy.service.d state: directory mode: '0755' + when: not dns_challenge_needed | bool -- name: Configure Caddy systemd override +- name: Configure Caddy systemd override (for standard installation) template: src: systemd-override.conf.j2 dest: /etc/systemd/system/caddy.service.d/override.conf mode: '0644' + when: not dns_challenge_needed | bool notify: - reload systemd - restart caddy @@ -86,7 +98,7 @@ src: Caddyfile.j2 dest: "{{ caddy_config_file }}" owner: root - group: "{{ caddy_group }}" + group: "{{ caddy_user }}" mode: '0640' backup: yes notify: reload caddy diff --git a/roles/caddy/templates/Caddyfile.j2 b/roles/caddy/templates/Caddyfile.j2 index 1c80caa..8162276 100644 --- a/roles/caddy/templates/Caddyfile.j2 +++ b/roles/caddy/templates/Caddyfile.j2 @@ -3,24 +3,30 @@ # Global configuration { - admin {{ caddy_admin_listen }} - {% if caddy_tls_enabled and caddy_tls_email %} # ACME configuration for Let's Encrypt email {{ caddy_tls_email }} acme_ca {{ caddy_acme_ca }} {% endif %} + {% if caddy_dns_provider == "cloudflare" and cloudflare_api_token %} + # DNS challenge for wildcard certificates + acme_dns cloudflare {{ cloudflare_api_token }} + {% endif %} + {% if not caddy_auto_https %} auto_https off {% endif %} } +# Import service configurations +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 (secure: no environment files) + # DNS challenge for automatic TLS tls { dns cloudflare {{ cloudflare_api_token }} resolvers {{ caddy_dns_resolvers | join(' ') }} @@ -29,8 +35,6 @@ # HTTP challenge for automatic TLS tls {{ caddy_tls_email }} {% endif %} - {% else %} - # TLS disabled {% endif %} # Serve static content @@ -57,83 +61,3 @@ {% endif %} } } - -# Additional configured sites -{% for site in caddy_sites %} -{{ site.domain }}{% if site.port is defined %}:{{ site.port }}{% endif %} { - {% if caddy_tls_enabled and site.tls != "off" %} - {% if site.dns_challenge | default(false) and caddy_dns_provider == "cloudflare" and cloudflare_api_token %} - # DNS challenge for this site (secure: direct variable substitution) - tls { - dns cloudflare {{ cloudflare_api_token }} - resolvers {{ caddy_dns_resolvers | join(' ') }} - } - {% elif caddy_tls_email and site.tls != "off" %} - # HTTP challenge for this site - tls {{ caddy_tls_email }} - {% endif %} - {% elif site.tls == "off" %} - # TLS explicitly disabled for this site - tls off - {% endif %} - - {% if site.root is defined %} - # Static file serving - root * {{ site.root }} - file_server - {% endif %} - - {% if site.backend is defined %} - # Reverse proxy - reverse_proxy {{ site.backend }} { - # Standard proxy headers - header_up Host {upstream_hostport} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - header_up X-Forwarded-Host {host} - } - {% endif %} - - # Logging for this site - log { - {% if caddy_log_format == "json" %} - output file {{ caddy_log_dir }}/{{ site.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 }}/{{ site.domain | replace('.', '_') }}.log { - roll_size 100mb - roll_keep 5 - } - level {{ caddy_log_level }} - {% endif %} - } - - {% if site.extra_config is defined %} - # Additional site configuration - {{ site.extra_config | indent(4) }} - {% endif %} -} - -{% endfor %} - -{% if caddy_tls_enabled %} -# HTTP to HTTPS redirects -http://{{ caddy_domain }} { - redir https://{host}{uri} permanent -} - -{% for site in caddy_sites %} -{% if site.tls != "off" %} -http://{{ site.domain }} { - redir https://{host}{uri} permanent -} -{% endif %} -{% endfor %} -{% endif %} \ No newline at end of file diff --git a/roles/caddy/templates/caddy.service.j2 b/roles/caddy/templates/caddy.service.j2 new file mode 100644 index 0000000..cc1d105 --- /dev/null +++ b/roles/caddy/templates/caddy.service.j2 @@ -0,0 +1,69 @@ +# Custom Caddy systemd service file +# Combines official Caddy service with enhanced security and API persistence +# Generated by Ansible - DO NOT EDIT MANUALLY + +[Unit] +Description=Caddy web server +Documentation=https://caddyserver.com/docs/ +After=network.target network-online.target +Requires=network-online.target + +[Service] +Type=notify +User={{ caddy_user }} +Group={{ caddy_user }} +ExecStart=/usr/bin/caddy run --environ --config {{ caddy_config_file }}{% if caddy_api_enabled %} --resume{% endif %} + +ExecReload=/usr/bin/caddy reload --config {{ caddy_config_file }} --force + +{% if caddy_api_enabled %} +# Wait for API to be ready before considering service started +ExecStartPost=/bin/bash -c 'until curl -s http://{{ caddy_admin_listen }}/config/ >/dev/null 2>&1; do sleep 1; done' +{% endif %} + +# Standard settings from official service +TimeoutStopSec=5s +LimitNOFILE=1048576 +PrivateTmp=true + +{% if caddy_systemd_security | default(true) %} +# Enhanced security hardening +NoNewPrivileges=true +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +RemoveIPC=true + +# Filesystem restrictions (upgrade from ProtectSystem=full) +ProtectSystem=strict +ProtectHome=true +ReadWritePaths={{ caddy_data_dir }} {{ caddy_log_dir }} +BindReadOnlyPaths={{ caddy_config_dir }} +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true +ProtectClock=true + +# Network and namespace restrictions +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true + +# Process restrictions +LimitNPROC=1048576 +MemoryDenyWriteExecute=true +SystemCallFilter=@system-service +SystemCallErrorNumber=EPERM + +# Logging (explicit configuration) +StandardOutput=journal +StandardError=journal +SyslogIdentifier=caddy +{% else %} +# Standard security from official service +ProtectSystem=full +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +{% endif %} + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/caddy/templates/systemd-override.conf.j2 b/roles/caddy/templates/systemd-override.conf.j2 index e51d32e..e4b5c2e 100644 --- a/roles/caddy/templates/systemd-override.conf.j2 +++ b/roles/caddy/templates/systemd-override.conf.j2 @@ -1,8 +1,13 @@ [Service] -# Reload configuration with --force flag for reliability +ExecStart= +ExecStart=/usr/bin/caddy run --config {{ caddy_config_file }} --resume + ExecReload= ExecReload=/usr/bin/caddy reload --config {{ caddy_config_file }} --force +# Wait for API to be ready before considering service started +ExecStartPost=/bin/bash -c 'until curl -s http://{{ caddy_admin_listen }}/config/ >/dev/null 2>&1; do sleep 1; done' + {% if caddy_systemd_security | default(true) %} # Enhanced security hardening beyond base service NoNewPrivileges=true @@ -35,4 +40,4 @@ SystemCallErrorNumber=EPERM StandardOutput=journal StandardError=journal SyslogIdentifier=caddy -{% endif %} \ No newline at end of file +{% endif %} diff --git a/site.yml b/site.yml index 187014f..28ad20d 100644 --- a/site.yml +++ b/site.yml @@ -1,13 +1,38 @@ --- -- name: Secure VPS Infrastructure Setup +# Core infrastructure deployment with security hardening first + +# Security hardening establishes secure foundation before web services +- import_playbook: playbooks/security.yml + +- name: Deploy Core Infrastructure hosts: arch-vps become: yes gather_facts: yes roles: - role: caddy - tags: ['caddy', 'web', 'https'] + tags: ['caddy', 'infrastructure', 'web'] - # Optional: Include security playbook - # - import_playbook: playbooks/security.yml - # tags: ['security', 'firewall', 'ssh'] + post_tasks: + - name: Verify Caddy API is accessible + uri: + url: "http://{{ caddy_admin_listen }}/config/" + method: GET + status_code: 200 + retries: 5 + delay: 2 + + - name: Display infrastructure status + debug: + msg: | + ✅ Core infrastructure deployment completed! + + 🌐 Primary domain: {{ caddy_domain }} + 🔒 HTTPS: {{ 'Enabled with DNS challenge (' + caddy_dns_provider + ')' if caddy_dns_provider else 'Enabled with HTTP challenge' }} + 🚀 API registration: {{ 'Ready' if caddy_api_enabled else 'Disabled' }} + + 📍 Admin API: http://{{ caddy_admin_listen }} (localhost only) + 📁 Web root: {{ caddy_web_root }} + 📝 Logs: {{ caddy_log_dir }} + + 📖 Documentation: docs/caddy-api-registration.md