├── .ansible-lint ├── .github └── workflows │ └── push.yml ├── .gitignore ├── .releaserc.json ├── LICENSE ├── README.md ├── build.sh ├── defaults └── main.yml ├── handlers └── main.yml ├── meta └── main.yml ├── tasks ├── main.yml ├── setup.yml └── vhosts.yml └── templates ├── Caddyfile.j2 ├── caddy.service.j2 ├── reverse.j2 └── tls.conf.j2 /.ansible-lint: -------------------------------------------------------------------------------- 1 | skip_list: 2 | - "106" # Role name {} does not match ``^[a-z][a-z0-9_]+$`` pattern' 3 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push workflow 2 | 3 | on: push 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | name: ansible-lint 9 | steps: 10 | - uses: actions/checkout@master 11 | - uses: actions/setup-python@v5 12 | - run: pip install ansible ansible-lint 13 | - run: ansible-lint --version 14 | - run: ansible-lint . 15 | 16 | commitlint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Lint Commit 24 | uses: wagoid/commitlint-github-action@v3 25 | 26 | release: 27 | name: Publish new release 28 | needs: [lint, commitlint] 29 | if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | - name: Release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | run: npx semantic-release 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "plugins": [ 6 | "@semantic-release/commit-analyzer", 7 | "@semantic-release/release-notes-generator", 8 | [ 9 | "@semantic-release/github", 10 | { 11 | "successComment": false, 12 | "releasedLabels": false 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 angristan (Stanislas Lange) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible role for Caddy 2 2 | 3 | This is a role I made for myself but I tried to make it as reusable as possible while keeping it fitted to my use. 4 | 5 | The role will handle all basic config like creating a systemd service, a user, conf folders, conf files, log folders, etc. 6 | 7 | Then, you can use it to add vhosts using templates. The role include one sample reverse proxy template. 8 | 9 | The role should work on all Debian-based distributions. 10 | 11 | ## Requirements 12 | 13 | This role does not install Caddy from APT because I want the cloudflare module. Run `build.sh` to build a caddy binary. 14 | 15 | ## Role Variables 16 | 17 | Basic configuration: 18 | 19 | - `caddy_bin_path`: caddy binary path (`/usr/bin/caddy`) 20 | - `caddy_log_path`: log directory (`/var/log/caddy`) 21 | - `caddy_config_path`: configuration directory (`/etc/caddy`) 22 | 23 | A user will be created (`caddy_user_name`), added to a group (`caddy_group_name`) with a specific UID (`caddy_user_id`) and GID (`caddy_group_id`). The default is `caddy/caddy` and `333/333`. 24 | 25 | Use this config to use the Cloudflare API for the DNS-01 ACME challenge: 26 | 27 | ```yaml 28 | cloudflare_token: xxx 29 | caddy_tls_dns_cloudflare_enabled: true 30 | caddy_env_vars: 31 | - "CLOUDFLARE_API_TOKEN={{ cloudflare_token }}" 32 | ``` 33 | 34 | Otherwise, Caddy will default to HTTP-01 or TLS-ALPN-01. 35 | 36 | Vhosts configuration: 37 | 38 | - `caddy_vhosts`: list of vhosts. (`[]`) 39 | - `caddy_rm_unmanaged_vhosts`: remove unmanaged vhosts (default `false`) 40 | 41 | Example: 42 | 43 | ```yml 44 | caddy_vhosts: 45 | - name: site1 46 | hostname: site1.domain.tld 47 | proxy_host: http://10.0.0.1 48 | gzip: compress 49 | security_headers: true 50 | responds: ["/forbidden 403"] 51 | rewrites: ["* /path{uri}"] 52 | - name: site2 53 | hostname: site1.domain.tld 54 | ansible.builtin.template: custom_template.j2 55 | ``` 56 | 57 | By default, the vhosts will use the `reverse.j2` template included in the role. Look at it and the `defaults/main.yml` file for all variables! 58 | 59 | - `caddy_vhost_defaults`: default vhost parameters. For each vhost in `caddy_vhosts`, it will be combined with the vhost's parameters. If a vhost defines an option that exist in `caddy_vhost_defaults`, the vhost option will overwrite the default one. 60 | 61 | ## Example playbook 62 | 63 | ```yaml 64 | --- 65 | - hosts: myhost 66 | roles: 67 | - { role: angristan.caddy, tags: caddy } 68 | vars: 69 | caddy_vhosts: 70 | - name: "website" 71 | hostname: "website.tld" 72 | ``` 73 | 74 | ## Usage 75 | 76 | Add this to `requirements.yml`: 77 | 78 | ```yml 79 | - src: https://github.com/angristan/ansible-caddy 80 | name: angristan.caddy 81 | version: vX.X.X 82 | ``` 83 | 84 | ## Author Information 85 | 86 | See my other Ansible roles at [angristan/ansible-roles](https://github.com/angristan/ansible-roles). 87 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | # docker run --rm -v $(pwd)/bin:/output caddy:2.2.1-builder sh -c "caddy-builder github.com/caddy-dns/cloudflare github.com/greenpau/caddy-auth-jwt github.com/greenpau/caddy-auth-portal && cp /usr/bin/caddy /output/" 2 | 3 | export GOOS=linux 4 | export GOARCH=amd64 5 | 6 | go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest 7 | xcaddy build --with github.com/caddy-dns/cloudflare --output bin/caddy 8 | # xcaddy build --with github.com/caddy-dns/cloudflare --with github.com/lindenlab/caddy-s3-proxy --output bin/caddy 9 | # xcaddy build --with github.com/caddy-dns/cloudflare --with github.com/greenpau/caddy-auth-jwt --with github.com/greenpau/caddy-auth-portal --output bin/caddy-auth 10 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Config 3 | caddy_src_bin: bin/caddy 4 | caddy_bin_path: /usr/bin/caddy 5 | caddy_log_path: /var/log/caddy 6 | caddy_config_path: /etc/caddy 7 | caddy_user_name: caddy 8 | caddy_group_name: www-data 9 | caddy_user_home_dir: /var/lib/caddy 10 | caddy_user_id: 333 11 | caddy_group_id: 33 12 | caddy_env_vars: [] 13 | caddy_service_after_units: [] 14 | caddy_metrics_enabled: true 15 | 16 | # TLS 17 | caddy_tls_dns_cloudflare_enabled: false 18 | 19 | # Vhosts 20 | caddy_vhosts: [] 21 | caddy_vhost_defaults: 22 | template: reverse.j2 23 | compress: true 24 | proxy_host: null 25 | proxy_transparent_disable: false 26 | security_headers: false 27 | basicauth: false 28 | basicauth_path: "/" 29 | www_redir: false 30 | root: null 31 | php_fastcgi_enabled: null 32 | php_fastcgi_path: null 33 | responds: [] 34 | rewrites: [] 35 | file_server: false 36 | file_server_browse: false 37 | protected_enabled: false 38 | protected_remote_ips: [] 39 | protected_header_name: null 40 | protected_header_value: null 41 | bind: null 42 | caddy_rm_unmanaged_vhosts: false 43 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reload caddy 3 | ansible.builtin.service: 4 | name: caddy 5 | state: reloaded 6 | 7 | - name: Restart caddy 8 | listen: service modified 9 | ansible.builtin.service: 10 | name: caddy 11 | state: restarted 12 | 13 | - name: Run systemctl daemon-reload 14 | listen: service modified 15 | ansible.builtin.systemd: 16 | daemon_reload: true 17 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | role_name: caddy 3 | author: angristan 4 | description: Ansible role for Caddy 5 | license: MIT 6 | min_ansible_version: "2.4" 7 | 8 | platforms: 9 | - name: Debian 10 | versions: 11 | - all 12 | - name: Ubuntu 13 | versions: 14 | - all 15 | 16 | galaxy_tags: 17 | - caddy 18 | - debian 19 | - ubuntu 20 | 21 | dependencies: [] 22 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Caddy 3 | ansible.builtin.import_tasks: setup.yml 4 | tags: caddy.setup 5 | 6 | - name: Clean up old Caddy vhost files 7 | tags: caddy.vhosts 8 | ansible.builtin.find: 9 | paths: "{{ caddy_config_path }}/vhosts" 10 | patterns: "*" 11 | register: existing_vhosts 12 | when: caddy_rm_unmanaged_vhosts 13 | 14 | - name: Remove old Caddy vhost files 15 | tags: caddy.vhosts 16 | ansible.builtin.file: 17 | path: "{{ item.path }}" 18 | state: absent 19 | loop: "{{ existing_vhosts.files | default([]) }}" 20 | when: item.path | basename not in caddy_vhosts | map(attribute='name') | list 21 | 22 | - name: Configure Caddy 23 | ansible.builtin.include_tasks: vhosts.yml 24 | tags: caddy.vhosts 25 | vars: 26 | vhost: "{{ caddy_vhost_defaults | combine(vhost_) }}" 27 | loop: "{{ caddy_vhosts }}" 28 | loop_control: 29 | loop_var: vhost_ 30 | -------------------------------------------------------------------------------- /tasks/setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Upload caddy binary 3 | ansible.builtin.copy: 4 | src: "{{ caddy_src_bin }}" 5 | dest: "{{ caddy_bin_path }}" 6 | mode: "0755" 7 | notify: Restart caddy 8 | 9 | - name: Add caddy group 10 | ansible.builtin.group: 11 | name: "{{ caddy_group_name }}" 12 | gid: "{{ caddy_group_id }}" 13 | 14 | - name: Add caddy user 15 | ansible.builtin.user: 16 | name: "{{ caddy_user_name }}" 17 | uid: "{{ caddy_user_id }}" 18 | group: "{{ caddy_group_id }}" 19 | home: "{{ caddy_user_home_dir }}" 20 | create_home: true 21 | shell: "/usr/sbin/nologin" 22 | system: true 23 | 24 | - name: Create Caddy systemd unit file 25 | ansible.builtin.template: 26 | src: caddy.service.j2 27 | dest: /etc/systemd/system/caddy.service 28 | mode: "0644" 29 | notify: service modified 30 | 31 | - name: Create config directory 32 | ansible.builtin.file: 33 | path: "{{ caddy_config_path }}" 34 | state: directory 35 | mode: "0755" 36 | 37 | - name: Create snippets directory 38 | ansible.builtin.file: 39 | path: "{{ caddy_config_path }}/snippets" 40 | state: directory 41 | mode: "0755" 42 | 43 | - name: Create vhosts directory 44 | ansible.builtin.file: 45 | path: "{{ caddy_config_path }}/vhosts" 46 | state: directory 47 | mode: "0755" 48 | 49 | - name: Create log directory 50 | ansible.builtin.file: 51 | path: "{{ caddy_log_path }}" 52 | state: directory 53 | mode: "0755" 54 | owner: caddy 55 | 56 | - name: Add Caddyfile 57 | ansible.builtin.template: 58 | src: Caddyfile.j2 59 | dest: "{{ caddy_config_path }}/Caddyfile" 60 | mode: "0644" 61 | notify: Reload caddy 62 | 63 | - name: Copy TLS config snippet 64 | ansible.builtin.template: 65 | mode: "0644" 66 | src: tls.conf.j2 67 | dest: "{{ caddy_config_path }}/snippets/tls.conf" 68 | notify: Reload caddy 69 | 70 | - name: Enable and start Caddy service 71 | ansible.builtin.service: 72 | name: caddy.service 73 | state: started 74 | enabled: true 75 | -------------------------------------------------------------------------------- /tasks/vhosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add vhost 3 | ansible.builtin.template: 4 | src: "{{ vhost.template }}" 5 | dest: "{{ caddy_config_path }}/vhosts/{{ vhost.name }}" 6 | mode: "0644" 7 | notify: Reload caddy 8 | tags: caddy.vhosts 9 | -------------------------------------------------------------------------------- /templates/Caddyfile.j2: -------------------------------------------------------------------------------- 1 | {% if caddy_metrics_enabled %} 2 | { 3 | servers { 4 | metrics 5 | } 6 | } 7 | {% endif %} 8 | 9 | import vhosts/* 10 | -------------------------------------------------------------------------------- /templates/caddy.service.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment(decoration="# ") }} 2 | 3 | [Unit] 4 | Description=Caddy 5 | Documentation=https://caddyserver.com/docs/ 6 | After=network.target {% for unit in caddy_service_after_units %}{{ unit }}{% endfor %}{{''}} 7 | StartLimitIntervalSec=60 8 | StartLimitBurst=10 9 | 10 | [Service] 11 | User={{ caddy_user_name }} 12 | Group={{ caddy_group_name }} 13 | ExecStart={{ caddy_bin_path }} run --environ --config {{ caddy_config_path }}/Caddyfile 14 | ExecReload={{ caddy_bin_path }} reload --config {{ caddy_config_path }}/Caddyfile 15 | TimeoutStopSec=5s 16 | LimitNOFILE=1048576 17 | LimitNPROC=512 18 | PrivateTmp=true 19 | ProtectSystem=full 20 | AmbientCapabilities=CAP_NET_BIND_SERVICE 21 | {% for caddy_env_var in caddy_env_vars %} 22 | Environment={{ caddy_env_var }} 23 | {% endfor %} 24 | Restart=always 25 | RestartSec=5 26 | 27 | [Install] 28 | WantedBy=multi-user.target 29 | -------------------------------------------------------------------------------- /templates/reverse.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment(decoration="# ") }} 2 | 3 | {% if vhost.www_redir %} 4 | www.{{ vhost.hostname }} { 5 | import ../snippets/tls.conf 6 | redir https://{{ vhost.hostname }}{uri} permanent 7 | } 8 | {% endif %} 9 | 10 | {{ vhost.hostname }} { 11 | import ../snippets/tls.conf 12 | {{ 'encode zstd gzip' if vhost.compress else '' }} 13 | log { 14 | output file {{ caddy_log_path }}/{{ vhost.hostname }}.log { 15 | # roll_disabled 16 | } 17 | } 18 | 19 | {% if vhost.bind %} 20 | bind {{ vhost.bind }} 21 | {% endif -%} 22 | 23 | {% if vhost.root %} 24 | root {{ vhost.root }} 25 | {% endif -%} 26 | 27 | {% if vhost.file_server %} 28 | {% if vhost.file_server_browse %} 29 | file_server browse 30 | {% else %} 31 | file_server 32 | {% endif -%} 33 | {% endif -%} 34 | 35 | {% for respond in vhost.responds %} 36 | respond {{ respond }} 37 | {% endfor -%} 38 | 39 | {% for rewrite in vhost.rewrites %} 40 | rewrite {{ rewrite }} 41 | {% endfor %} 42 | 43 | {% if vhost.protected_enabled %} 44 | @protected { 45 | {% if vhost.protected_remote_ips|length > 0 %} 46 | remote_ip forwarded {% for ip in vhost.protected_remote_ips %}{{ ip }} {% endfor %} 47 | 48 | {% endif %} 49 | {% if vhost.protected_header_name %} 50 | header {{ vhost.protected_header_name }} {{ vhost.protected_header_value }} 51 | {% endif %} 52 | } 53 | {% endif %} 54 | 55 | {% if vhost.proxy_host %} 56 | {% if vhost.protected_enabled %} 57 | handle @protected { 58 | {% endif %} 59 | reverse_proxy {{ vhost.proxy_host }}{% if vhost.proxy_transparent_disable %} { 60 | header_up -X-Forwarded-For 61 | header_up -X-Forwarded-Proto 62 | header_up -Host 63 | } {% endif %} 64 | {% if vhost.protected_enabled %} 65 | 66 | } 67 | {% endif %} 68 | {% endif %} 69 | 70 | {% if vhost.php_fastcgi_enabled %} 71 | {% if vhost.php_fastcgi_path %} 72 | php_fastcgi {{ vhost.php_fastcgi_path }} 73 | {% else %} 74 | php_fastcgi unix//run/php/php-fpm-{{ vhost.name }}.sock 75 | {% endif %} 76 | {% endif %} 77 | 78 | {%- if vhost.security_headers %} 79 | header { 80 | # enable HSTS 81 | Strict-Transport-Security max-age=31536000; 82 | 83 | # disable clients from sniffing the media type 84 | X-Content-Type-Options nosniff 85 | 86 | # clickjacking protection 87 | X-Frame-Options DENY 88 | 89 | # keep referrer data off of HTTP connections 90 | Referrer-Policy no-referrer-when-downgrade 91 | } 92 | {% endif %} 93 | 94 | {%- if vhost.basicauth %} 95 | basicauth {{ vhost.basicauth_path }}* { 96 | {{ vhost.basicauth_user }} {{ vhost.basicauth_password }} 97 | } 98 | {% endif %} 99 | 100 | {% if vhost.protected_enabled %} 101 | respond 403 102 | {% endif %} 103 | } 104 | -------------------------------------------------------------------------------- /templates/tls.conf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment(decoration="# ") }} 2 | tls { 3 | {% if caddy_tls_dns_cloudflare_enabled -%} 4 | dns cloudflare {env.CLOUDFLARE_API_TOKEN} 5 | {% endif -%} 6 | } 7 | --------------------------------------------------------------------------------