Skip to content

Commit c994174

Browse files
authored
Add Caddy reverse proxy role (#217)
Signed-off-by: Webster Mudge <[email protected]>
1 parent 051af71 commit c994174

File tree

10 files changed

+423
-0
lines changed

10 files changed

+423
-0
lines changed

roles/caddy/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# caddy
2+
3+
Install Caddy proxy packages.
4+
5+
This role installs the Caddy web server and reverse proxy, configuring it for self-signed TLS by default, or optionally with external CA certificates, or via Let's Encrypt. It sets up an initial global configuration with an import directory, configures a WWW root directory, and can retrieve Caddy's generated self-signed CA certificate to the target host if applicable.
6+
7+
The role will:
8+
- Install the Caddy proxy service.
9+
- Configure an initial global configuration for Caddy, including an import directory for modular configuration.
10+
- Set up a designated WWW root directory for serving static content.
11+
- Manage TLS certificates based on the `caddy_self_signed`, `caddy_ca_pem`, and `caddy_ca_key` parameters:
12+
- If `caddy_self_signed` is `true` (default) and `caddy_ca_pem` is not defined, Caddy will generate its own self-signed root CA and issue certificates. The Caddy self-signed CA certificate will be retrieved to the target host.
13+
- If `caddy_self_signed` is `false`, Caddy will attempt to use Let's Encrypt's ACME service to obtain trusted certificates.
14+
- If `caddy_ca_pem` and `caddy_ca_key` are provided, Caddy will use these external CA credentials for TLS.
15+
- Ensure the Caddy service is running and enabled.
16+
17+
# Requirements
18+
19+
- A DNS A record resolving `caddy_domain` to the target host's IP address is recommended for proper certificate validation.
20+
- Ports 80 and 443 must be open on the target host and accessible for inbound connections.
21+
- For Let's Encrypt (when `caddy_self_signed` is `false`), ports 80/443 must be publicly accessible and the domain must be resolvable via public DNS.
22+
23+
# Dependencies
24+
25+
None.
26+
27+
# Parameters
28+
29+
| Variable | Type | Required | Default | Description |
30+
| --- | --- | --- | --- | --- |
31+
| `caddy_domain` | `str` | `True` | | Domain name for the Caddy reverse proxy (e.g., `proxy.example.com`). |
32+
| `caddy_www_root` | `path` | `False` | `/var/www_root` | Directory where static WWW service content will be served from. |
33+
| `caddy_self_signed` | `bool` | `False` | `true` | Flag enabling Caddy to issue self-signed TLS certificates. If `false`, Caddy defaults to using the Let's Encrypt ACME service. If `true` and `caddy_ca_pem` is not defined, Caddy generates its own root CA. |
34+
| `caddy_ca_pem` | `path` | `False` | | Path to an external CA certificate file (PEM format) to be used by Caddy for TLS. |
35+
| `caddy_ca_key` | `path` | `False` | | Path to the private key for the external CA (`caddy_ca_pem`). This parameter is required if `caddy_ca_pem` is defined. |
36+
37+
# Example Playbook
38+
39+
```yaml
40+
- hosts: proxy_servers
41+
tasks:
42+
- name: Install Caddy with default self-signed TLS
43+
ansible.builtin.import_role:
44+
name: cloudera.exe.caddy
45+
vars:
46+
caddy_domain: "dev-proxy.example.com"
47+
# caddy_self_signed will default to true
48+
# caddy_www_root will default to /var/www_root
49+
50+
- name: Install Caddy using Let's Encrypt
51+
ansible.builtin.import_role:
52+
name: cloudera.exe.caddy
53+
vars:
54+
caddy_domain: "prod-proxy.example.com"
55+
caddy_self_signed: false # Enable Let's Encrypt ACME
56+
57+
- name: Install Caddy with external CA certificates
58+
ansible.builtin.import_role:
59+
name: cloudera.exe.caddy
60+
vars:
61+
caddy_domain: "internal-proxy.example.com"
62+
caddy_self_signed: true # Still technically self-signed, but by an external CA
63+
caddy_ca_pem: "/path/to/my_org_ca.pem"
64+
caddy_ca_key: "/path/to/my_org_ca.key"
65+
caddy_www_root: "/srv/my_app_html"
66+
```
67+
68+
# License
69+
70+
```
71+
Copyright 2025 Cloudera, Inc.
72+
73+
Licensed under the Apache License, Version 2.0 (the "License");
74+
you may not use this file except in compliance with the License.
75+
You may obtain a copy of the License at
76+
77+
https://www.apache.org/licenses/LICENSE-2.0
78+
79+
Unless required by applicable law or agreed to in writing, software
80+
distributed under the License is distributed on an "AS IS" BASIS,
81+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
82+
See the License for the specific language governing permissions and
83+
limitations under the License.
84+
```

roles/caddy/defaults/main.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2025 Cloudera, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
---
16+
17+
caddy_self_signed: true
18+
caddy_domain: "{{ undef(hint='Please provide the default domain for the Caddy service') }}"
19+
caddy_www_root: /var/www_root
20+
# caddy_ca_pem: # path
21+
# caddy_ca_key: # path

roles/caddy/handlers/main.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2025 Cloudera, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
---
16+
17+
- name: Reload Caddy
18+
ansible.builtin.service:
19+
name: "{{ caddy_service }}"
20+
state: reloaded
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright 2025 Cloudera, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
argument_specs:
16+
main:
17+
short_description: Install Caddy proxy packages.
18+
description:
19+
- Install Caddy proxy service using self-signed TLS.
20+
- Configure an initial global configuration with an import directory.
21+
- Configure a WWW root directory.
22+
- Optionally, provision the Caddy with external CA certificates.
23+
- If Caddy self-signing is enabled and an external CA certificate is not defined, retrieve the Caddy self-signed CA certificate to the target host.
24+
author: Cloudera Labs
25+
version_added: "5.0.0"
26+
options:
27+
caddy_domain:
28+
description: Domain name for the Caddy reverse proxy.
29+
type: str
30+
required: true
31+
caddy_www_root:
32+
description: Directory of the static WWW service.
33+
type: path
34+
required: false
35+
default: /var/www_root
36+
caddy_self_signed:
37+
description:
38+
- Flag enabling Caddy to issue self-signed TLS certificates.
39+
- If not defined, Caddy will default to the Let's Encrypt ACME service.
40+
- If defined and O(caddy_ca_pem) is not defined, Caddy will generate a root CA certificate for self-signed TLS.
41+
type: bool
42+
required: false
43+
default: true
44+
caddy_ca_pem:
45+
description:
46+
- External CA for the Caddy TLS service.
47+
type: path
48+
required: false
49+
caddy_ca_key:
50+
description:
51+
- External CA key for the Caddy TLS service.
52+
- Required if O(caddy_ca_pem) is defined.
53+
type: path
54+
required: false

roles/caddy/tasks/RedHat-pre.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2025 Cloudera, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
---
16+
17+
- name: Enable Fedora COPR
18+
ansible.builtin.package:
19+
name: "dnf-command(copr)"
20+
21+
- name: Enable Caddy project in COPR
22+
ansible.builtin.command: "dnf copr enable -y @caddy/caddy"
23+
changed_when: false

roles/caddy/tasks/default-pre.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2025 Cloudera, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
---
16+
17+
# no op

roles/caddy/tasks/main.yml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright 2025 Cloudera, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
---
16+
17+
- name: Gather host distribution details
18+
ansible.builtin.setup:
19+
gather_subset: distribution
20+
21+
- name: Load distribution variables
22+
ansible.builtin.include_vars: "{{ item }}"
23+
with_first_found:
24+
- "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}.yml"
25+
- "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
26+
- "{{ ansible_facts['distribution'] }}.yml"
27+
- "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_version'] }}.yml"
28+
- "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
29+
- "{{ ansible_facts['os_family'] }}.yml"
30+
- "default.yml"
31+
32+
- name: Run distribution pre-tasks
33+
ansible.builtin.include_tasks: "{{ item }}"
34+
with_first_found:
35+
- "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}-pre.yml"
36+
- "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}-pre.yml"
37+
- "{{ ansible_facts['distribution'] }}-pre.yml"
38+
- "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_version'] }}-pre.yml"
39+
- "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_major_version'] }}-pre.yml"
40+
- "{{ ansible_facts['os_family'] }}-pre.yml"
41+
- "default-pre.yml"
42+
43+
- name: Install Caddy binaries
44+
ansible.builtin.package:
45+
name: "{{ caddy_package }}"
46+
loop: "{{ caddy_packages }}"
47+
loop_control:
48+
loop_var: caddy_package
49+
50+
- name: Set up Caddyfile imports directory
51+
ansible.builtin.file:
52+
path: "/etc/caddy/Caddyfile.d"
53+
mode: "0755"
54+
state: directory
55+
56+
- name: Set up Caddy WWW root directory
57+
ansible.builtin.file:
58+
path: "{{ caddy_www_root }}"
59+
mode: "0755"
60+
state: directory
61+
62+
- name: Set up default index page for Caddy WWW root
63+
ansible.builtin.file:
64+
path: "{{ [caddy_www_root, 'index.html'] | path_join }}"
65+
mode: "0755"
66+
state: touch
67+
68+
- name: Provision external CA certificates
69+
when: caddy_ca_pem is defined
70+
block:
71+
- name: Set up external PKI directory
72+
ansible.builtin.file:
73+
path: "{{ caddy_external_pki_dir }}"
74+
state: directory
75+
owner: caddy
76+
group: caddy
77+
mode: "0700"
78+
79+
- name: Install external CA certificates
80+
ansible.builtin.copy:
81+
src: "{{ __ca_file }}"
82+
dest: "{{ [caddy_external_pki_dir, __ca_file | basename] | path_join }}"
83+
owner: caddy
84+
group: caddy
85+
mode: "0600"
86+
loop:
87+
- "{{ caddy_ca_pem }}"
88+
- "{{ caddy_ca_key }}"
89+
loop_control:
90+
loop_var: __ca_file
91+
92+
# - name: Set up Caddyfile ACME issuers
93+
# ansible.builtin.blockinfile:
94+
# backup: no
95+
# path: "/etc/caddy/Caddyfile"
96+
# insertbefore: BOF
97+
# # append_newline: yes <=2.16
98+
# block: |
99+
# {
100+
# cert_issuer acme
101+
# cert_issuer acme {
102+
# dir https://acme.zerossl.com/v2/DV90
103+
104+
# }
105+
# }
106+
107+
# - name: Set up Caddy CA
108+
# when: caddy_self_signed
109+
# ansible.builtin.blockinfile:
110+
# backup: no
111+
# path: "/etc/caddy/Caddyfile"
112+
# insertbefore: BOF
113+
# # append_newline: yes <=2.16
114+
# block: "{{ lookup('template', 'internal_ca.json.j2') }}"
115+
116+
# - name: Set up Caddyfile imports directive
117+
# ansible.builtin.blockinfile:
118+
# backup: no
119+
# path: "/etc/caddy/Caddyfile"
120+
# insertafter: EOF
121+
# # prepend_newline: yes <=2.16
122+
# block: |
123+
# import Caddyfile.d/*.caddyfile
124+
125+
- name: Provision Caddy configuration
126+
ansible.builtin.template:
127+
src: Caddyfile.j2
128+
dest: /etc/caddy/Caddyfile
129+
mode: "0755"
130+
131+
- name: Enable and run the Caddy service
132+
ansible.builtin.service:
133+
name: "{{ caddy_service }}"
134+
enabled: true
135+
state: started
136+
137+
- name: Retrieve the Caddy self-signed CA certificate
138+
when: caddy_self_signed and caddy_ca_pem is undefined
139+
ansible.builtin.fetch:
140+
src: /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
141+
dest: "{{ [playbook_dir, name_prefix + '-caddy-root.crt'] | path_join }}"
142+
flat: true

roles/caddy/templates/Caddyfile.j2

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Managed by cloudera.exe.caddy
2+
3+
{% if caddy_self_signed %}
4+
{
5+
local_certs
6+
{% if caddy_ca_pem is defined %}
7+
pki {
8+
ca {
9+
root {
10+
cert {{ [caddy_external_pki_dir, caddy_ca_pem | basename] | path_join }}
11+
key {{ [caddy_external_pki_dir, caddy_ca_key | basename] | path_join }}
12+
}
13+
}
14+
}
15+
{% endif %}
16+
}
17+
{% endif %}
18+
19+
# Caddy default WWW root
20+
{{ caddy_domain }} {
21+
root * {{ caddy_www_root }}
22+
file_server
23+
}
24+
25+
# Caddy routes
26+
import Caddyfile.d/*.caddyfile

0 commit comments

Comments
 (0)