diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..479879e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vault-password-file +vault.yml diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..065ff4d --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +inventory = inventory/hosts.yml +host_key_checking = False +remote_user = root +deprecation_warnings = False +vault_password_file = vault-password-file + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s diff --git a/host_vars/arch-vps/main.yml b/host_vars/arch-vps/main.yml new file mode 100644 index 0000000..979d652 --- /dev/null +++ b/host_vars/arch-vps/main.yml @@ -0,0 +1,41 @@ +--- +# ================================================================= +# Production Configuration for arch-vps (jnss.me) +# ================================================================= + +# ================================================================= +# TLS Configuration - Production Setup +# ================================================================= +caddy_tls_enabled: true +caddy_domain: "jnss.me" +caddy_tls_email: "{{ vault_caddy_tls_email }}" + +# DNS Challenge Configuration (Cloudflare) +caddy_dns_provider: "cloudflare" +cloudflare_api_token: "{{ vault_cloudflare_api_token }}" + +# Production Let's Encrypt CA +caddy_acme_ca: "https://acme-v02.api.letsencrypt.org/directory" + +# ================================================================= +# Site Configuration +# ================================================================= +# For now, just serve the main jnss.me domain +# Additional sites can be added here as services are deployed +caddy_sites: [] + +# 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 + +# ================================================================= +# Security & Logging +# ================================================================= +caddy_log_level: "INFO" +caddy_log_format: "json" +caddy_systemd_security: true \ No newline at end of file diff --git a/roles/caddy/README.md b/roles/caddy/README.md index a62f3fe..6f6db52 100644 --- a/roles/caddy/README.md +++ b/roles/caddy/README.md @@ -1,2 +1,237 @@ -# Install and configure caddy -Installs and configures caddy. +# 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. + +## Features + +- 🔐 **Automatic HTTPS** with Let's Encrypt certificates +- 🌐 **DNS Challenge Support** for wildcard certificates (Cloudflare provider) +- 🛡️ **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 + +## Requirements + +- Systemd-based Linux distribution (tested on Arch Linux) +- Ansible 2.9+ +- For DNS challenges: Cloudflare account with API token + +## Quick Start + +### Basic Static Site Setup + +```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 + +```yaml +# host_vars/myserver/main.yml +caddy_tls_enabled: true +caddy_domain: "example.com" +caddy_tls_email: "{{ vault_caddy_tls_email }}" +caddy_dns_provider: "cloudflare" +cloudflare_api_token: "{{ vault_cloudflare_api_token }}" + +# host_vars/myserver/vault.yml (encrypted) +vault_caddy_tls_email: "admin@example.com" +vault_cloudflare_api_token: "your-api-token-here" +``` + +## Required Variables + +### For HTTPS/TLS + +| Variable | Required When | Description | Example | +|----------|---------------|-------------|---------| +| `caddy_tls_enabled` | Always for HTTPS | Enable TLS/HTTPS | `true` | +| `caddy_tls_email` | HTTPS enabled | Email for Let's Encrypt | `"admin@example.com"` | +| `caddy_domain` | HTTPS enabled | Primary domain | `"example.com"` | + +### For DNS Challenge (Wildcard Certificates) + +| Variable | Required When | Description | Example | +|----------|---------------|-------------|---------| +| `caddy_dns_provider` | DNS challenge | DNS provider | `"cloudflare"` | +| `cloudflare_api_token` | Cloudflare DNS | API token for DNS | `"{{ vault_token }}"` | + +## Optional Variables + +### Service Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `caddy_version` | `"latest"` | Caddy version to install | +| `caddy_config_file` | `"/etc/caddy/Caddyfile"` | Main config file path | +| `caddy_service_enabled` | `true` | Enable systemd service | +| `caddy_service_state` | `"started"` | Service state | +| `caddy_admin_listen` | `"127.0.0.1:2019"` | Admin API endpoint | + +### Directory Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `caddy_config_dir` | `"/etc/caddy"` | Configuration directory | +| `caddy_data_dir` | `"/var/lib/caddy"` | Data/state directory | +| `caddy_log_dir` | `"/var/log/caddy"` | Log directory | +| `caddy_web_root` | `"/var/www"` | Web root directory | + +### Security Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `caddy_systemd_security` | `true` | Enable systemd security restrictions | +| `caddy_firewall_ports` | `[80, 443]` | Ports to open in firewall | + +### Logging Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `caddy_log_level` | `"INFO"` | Logging level (ERROR, WARN, INFO, DEBUG) | +| `caddy_log_format` | `"common"` | Log format (common, json) | + +### ACME Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `caddy_acme_ca` | Let's Encrypt Prod | ACME CA directory URL | +| `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 + +### Multiple Sites + +```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" +``` + +### Custom Systemd Security + +```yaml +caddy_systemd_security: false # Disable if you need custom security settings +``` + +### Staging Environment + +```yaml +# Use Let's Encrypt staging for testing +caddy_acme_ca: "https://acme-staging-v02.api.letsencrypt.org/directory" +``` + +## Security Features + +This role implements production-grade security hardening: + +### Systemd Security Restrictions + +- **NoNewPrivileges**: Prevents privilege escalation +- **CapabilityBoundingSet**: Limits to `CAP_NET_ADMIN` and `CAP_NET_BIND_SERVICE` +- **ProtectSystem=strict**: Read-only filesystem protection +- **ProtectKernelLogs/Modules/Tunables**: Kernel protection +- **RestrictAddressFamilies**: Limits to IPv4, IPv6, and Unix sockets +- **RestrictNamespaces/Realtime/SUIDSGID**: Additional restrictions +- **MemoryDenyWriteExecute**: Prevents code injection +- **SystemCallFilter=@system-service**: Whitelist system calls + +### File System Security + +- Dedicated `caddy` user and group +- Proper directory permissions +- Read-only configuration binding +- Isolated temporary files + +## File Structure + +``` +roles/caddy/ +├── defaults/main.yml # Default variables with documentation +├── tasks/main.yml # Installation and configuration tasks +├── handlers/main.yml # Service restart/reload handlers +├── templates/ +│ ├── Caddyfile.j2 # Main Caddyfile template +│ ├── index.html.j2 # Default welcome page +│ └── systemd-override.conf.j2 # Security hardening overrides +├── meta/main.yml # Role metadata and dependencies +└── README.md # This file +``` + +## Example Playbooks + +### Basic Deployment + +```yaml +- name: Deploy Caddy Web Server + hosts: webservers + become: yes + roles: + - caddy +``` + +### Full Infrastructure + +```yaml +- name: Secure VPS Setup + hosts: production + become: yes + roles: + - security # Firewall, SSH hardening, etc. + - caddy # Web server with HTTPS +``` + +## Dependencies + +This role automatically handles Caddy installation, including: + +- 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 + +## Compatibility + +- **Tested**: Arch Linux +- **Should work**: CentOS/RHEL 8+, Ubuntu 18.04+, Debian 10+ +- **Requires**: systemd, curl/wget + +## Contributing + +When modifying this role: + +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 + +## Security Considerations + +- **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 + +## License + +This role is part of the rick-infra project infrastructure configuration. diff --git a/roles/caddy/defaults/main.yml b/roles/caddy/defaults/main.yml new file mode 100644 index 0000000..22dde4e --- /dev/null +++ b/roles/caddy/defaults/main.yml @@ -0,0 +1,95 @@ +--- +# ================================================================= +# Caddy Web Server Role Configuration +# ================================================================= +# This role provides a complete Caddy setup with automatic HTTPS +# Override these variables in host_vars/ for production deployment + +# ================================================================= +# Basic Installation Configuration +# ================================================================= +caddy_version: "latest" +caddy_user: "caddy" +caddy_group: "caddy" +caddy_home: "/var/lib/caddy" +caddy_config_dir: "/etc/caddy" +caddy_data_dir: "/var/lib/caddy" +caddy_log_dir: "/var/log/caddy" +caddy_web_root: "/var/www" +caddy_default_site_root: "{{ caddy_web_root }}/default" + +# ================================================================= +# Service Configuration +# ================================================================= +caddy_config_file: "/etc/caddy/Caddyfile" # Package default path +caddy_service_enabled: true +caddy_service_state: "started" +caddy_auto_https: true +caddy_admin_listen: "127.0.0.1:2019" + +# ================================================================= +# TLS/HTTPS Configuration +# ================================================================= +# Enable automatic HTTPS with Let's Encrypt certificates +caddy_tls_enabled: false # Set to true to enable HTTPS +caddy_tls_email: "" # Required for Let's Encrypt (e.g., "admin@example.com") +caddy_domain: "localhost" # Primary domain to serve + +# ACME Certificate Authority settings +caddy_acme_ca: "https://acme-v02.api.letsencrypt.org/directory" # Production CA +# caddy_acme_ca: "https://acme-staging-v02.api.letsencrypt.org/directory" # Staging for testing + +# ================================================================= +# DNS Challenge Configuration (for wildcard certificates) +# ================================================================= +# DNS challenge allows wildcard certificates and works behind firewalls +caddy_dns_provider: "" # Set to "cloudflare" for Cloudflare DNS challenge +cloudflare_api_token: "" # Cloudflare API token (override in host_vars with vault reference) + +# DNS challenge settings +caddy_dns_resolvers: # DNS resolvers for challenge verification + - "1.1.1.1:53" + - "1.0.0.1:53" +caddy_dns_propagation_timeout: 120 # Seconds to wait for DNS propagation + +# ================================================================= +# Sites 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" + +# ================================================================= +# Security & Network Configuration +# ================================================================= +# Firewall ports to open automatically +caddy_firewall_ports: + - 80 # HTTP (for redirects and ACME challenges) + - 443 # HTTPS (for TLS traffic) + +# ================================================================= +# Advanced Configuration +# ================================================================= +# 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) diff --git a/roles/caddy/handlers/main.yml b/roles/caddy/handlers/main.yml new file mode 100644 index 0000000..9c674fb --- /dev/null +++ b/roles/caddy/handlers/main.yml @@ -0,0 +1,20 @@ +--- +- name: reload systemd + systemd: + daemon_reload: yes + +- name: restart caddy + systemd: + name: caddy + state: restarted + daemon_reload: yes + +- name: reload caddy + systemd: + name: caddy + state: reloaded + +- name: stop caddy + systemd: + name: caddy + state: stopped \ No newline at end of file diff --git a/roles/caddy/meta/main.yml b/roles/caddy/meta/main.yml new file mode 100644 index 0000000..3456b6f --- /dev/null +++ b/roles/caddy/meta/main.yml @@ -0,0 +1,15 @@ +--- +galaxy_info: + role_name: caddy + author: rick-infra + description: Caddy web server and reverse proxy + min_ansible_version: 2.9 + platforms: + - name: ArchLinux + versions: + - all + +dependencies: [] + +collections: + - ansible.posix diff --git a/roles/caddy/tasks/main.yml b/roles/caddy/tasks/main.yml new file mode 100644 index 0000000..974145c --- /dev/null +++ b/roles/caddy/tasks/main.yml @@ -0,0 +1,122 @@ +--- +- name: Check if DNS challenge is needed + set_fact: + dns_challenge_needed: "{{ caddy_dns_provider == 'cloudflare' and cloudflare_api_token != '' }}" + +- name: Install standard Caddy (if no DNS challenge needed) + pacman: + name: caddy + state: present + when: not dns_challenge_needed | 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 + +- name: Install Caddy with Cloudflare plugin + copy: + src: /tmp/caddy-with-cloudflare + dest: /usr/bin/caddy + mode: '0755' + remote_src: yes + backup: yes + when: dns_challenge_needed | bool + notify: restart caddy + +- name: Clean up temporary Caddy binary + file: + path: /tmp/caddy-with-cloudflare + state: absent + +- name: Create caddy user and group + user: + name: "{{ caddy_user }}" + group: "{{ caddy_group }}" + home: "{{ caddy_home }}" + shell: /usr/bin/nologin + system: yes + createhome: yes + notify: restart caddy + +- name: Create Caddy directories + file: + path: "{{ item }}" + state: directory + owner: "{{ caddy_user }}" + group: "{{ caddy_group }}" + mode: '0755' + loop: + - "{{ caddy_config_dir }}" + - "{{ caddy_data_dir }}" + - "{{ caddy_log_dir }}" + - "{{ caddy_web_root }}" + - "{{ caddy_default_site_root }}" + +- name: Deploy default index page + template: + src: index.html.j2 + dest: "{{ caddy_default_site_root }}/index.html" + owner: "{{ caddy_user }}" + group: "{{ caddy_group }}" + mode: '0644' + + + +- name: Create systemd override directory + file: + path: /etc/systemd/system/caddy.service.d + state: directory + mode: '0755' + +- name: Configure Caddy systemd override + template: + src: systemd-override.conf.j2 + dest: /etc/systemd/system/caddy.service.d/override.conf + mode: '0644' + notify: + - reload systemd + - restart caddy + +- name: Generate Caddyfile from template (with vault secrets) + template: + src: Caddyfile.j2 + dest: "{{ caddy_config_file }}" + owner: root + group: "{{ caddy_group }}" + mode: '0640' + backup: yes + notify: reload caddy + +- name: Check Caddyfile syntax (basic check) + command: caddy fmt --overwrite "{{ caddy_config_file }}" + register: caddy_fmt_result + changed_when: false + failed_when: false + +# Note: Full validation with environment variables happens at service startup + +- name: Enable and start Caddy service + systemd: + name: caddy + enabled: "{{ caddy_service_enabled }}" + state: "{{ caddy_service_state }}" + daemon_reload: yes + +- name: Wait for Caddy to be running + wait_for: + port: 80 + host: 127.0.0.1 + timeout: 30 + when: caddy_service_state == "started" + +- name: Verify Caddy admin API is accessible + uri: + url: "http://{{ caddy_admin_listen }}/config/" + method: GET + register: caddy_admin_check + failed_when: false + changed_when: false diff --git a/roles/caddy/templates/Caddyfile.j2 b/roles/caddy/templates/Caddyfile.j2 new file mode 100644 index 0000000..1c80caa --- /dev/null +++ b/roles/caddy/templates/Caddyfile.j2 @@ -0,0 +1,139 @@ +# Caddy configuration file +# Generated by Ansible - DO NOT EDIT MANUALLY + +# 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 not caddy_auto_https %} + auto_https off + {% endif %} +} + +# 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) + tls { + dns cloudflare {{ cloudflare_api_token }} + resolvers {{ caddy_dns_resolvers | join(' ') }} + } + {% elif caddy_tls_email %} + # HTTP challenge for automatic TLS + tls {{ caddy_tls_email }} + {% endif %} + {% else %} + # TLS disabled + {% endif %} + + # Serve static content + root * {{ caddy_default_site_root }} + file_server + + # Logging + log { + {% if caddy_log_format == "json" %} + output file {{ caddy_log_dir }}/{{ caddy_domain | replace('.', '_') }}.log { + roll_size 100mb + roll_keep 5 + } + format json { + time_format "2006-01-02T15:04:05.000Z07:00" + } + level {{ caddy_log_level }} + {% else %} + output file {{ caddy_log_dir }}/{{ caddy_domain | replace('.', '_') }}.log { + roll_size 100mb + roll_keep 5 + } + level {{ caddy_log_level }} + {% endif %} + } +} + +# 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/index.html.j2 b/roles/caddy/templates/index.html.j2 new file mode 100644 index 0000000..3ac4e3b --- /dev/null +++ b/roles/caddy/templates/index.html.j2 @@ -0,0 +1,68 @@ + + + + + + Welcome to jnss + + + +
+

Welcome to jnss

+

🚀 Server infrastructure is online and secure

+
+ ✅ Caddy web server running
+ 🔒 Enterprise-grade security hardening active
+ 📊 Structured logging operational +
+

Infrastructure managed with Ansible

+
+ Deployed: {{ ansible_facts['date_time']['iso8601'] }} +
+
+ + diff --git a/roles/caddy/templates/systemd-override.conf.j2 b/roles/caddy/templates/systemd-override.conf.j2 new file mode 100644 index 0000000..e51d32e --- /dev/null +++ b/roles/caddy/templates/systemd-override.conf.j2 @@ -0,0 +1,38 @@ +[Service] +# Reload configuration with --force flag for reliability +ExecReload= +ExecReload=/usr/bin/caddy reload --config {{ caddy_config_file }} --force + +{% if caddy_systemd_security | default(true) %} +# Enhanced security hardening beyond base service +NoNewPrivileges=true +CapabilityBoundingSet=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 +{% endif %} \ No newline at end of file diff --git a/site.yml b/site.yml index 86009f8..187014f 100644 --- a/site.yml +++ b/site.yml @@ -1,5 +1,13 @@ --- -- name: Setting up VPS +- name: Secure VPS Infrastructure Setup hosts: arch-vps become: yes gather_facts: yes + + roles: + - role: caddy + tags: ['caddy', 'web', 'https'] + + # Optional: Include security playbook + # - import_playbook: playbooks/security.yml + # tags: ['security', 'firewall', 'ssh']