From abac1596f6b4f7b0e6061f9ab0d4824490876427 Mon Sep 17 00:00:00 2001 From: Webster Mudge Date: Thu, 31 Jul 2025 17:36:57 -0400 Subject: [PATCH] Add Caddy reverse proxy role Signed-off-by: Webster Mudge --- roles/caddy/README.md | 84 ++++++++++++++++ roles/caddy/defaults/main.yml | 21 ++++ roles/caddy/handlers/main.yml | 20 ++++ roles/caddy/meta/argument_specs.yml | 54 +++++++++++ roles/caddy/tasks/RedHat-pre.yml | 23 +++++ roles/caddy/tasks/default-pre.yml | 17 ++++ roles/caddy/tasks/main.yml | 142 ++++++++++++++++++++++++++++ roles/caddy/templates/Caddyfile.j2 | 26 +++++ roles/caddy/vars/RedHat.yml | 18 ++++ roles/caddy/vars/default.yml | 18 ++++ 10 files changed, 423 insertions(+) create mode 100644 roles/caddy/README.md create mode 100644 roles/caddy/defaults/main.yml create mode 100644 roles/caddy/handlers/main.yml create mode 100644 roles/caddy/meta/argument_specs.yml create mode 100644 roles/caddy/tasks/RedHat-pre.yml create mode 100644 roles/caddy/tasks/default-pre.yml create mode 100644 roles/caddy/tasks/main.yml create mode 100644 roles/caddy/templates/Caddyfile.j2 create mode 100644 roles/caddy/vars/RedHat.yml create mode 100644 roles/caddy/vars/default.yml diff --git a/roles/caddy/README.md b/roles/caddy/README.md new file mode 100644 index 00000000..4b724b5b --- /dev/null +++ b/roles/caddy/README.md @@ -0,0 +1,84 @@ +# caddy + +Install Caddy proxy packages. + +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. + +The role will: +- Install the Caddy proxy service. +- Configure an initial global configuration for Caddy, including an import directory for modular configuration. +- Set up a designated WWW root directory for serving static content. +- Manage TLS certificates based on the `caddy_self_signed`, `caddy_ca_pem`, and `caddy_ca_key` parameters: + - 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. + - If `caddy_self_signed` is `false`, Caddy will attempt to use Let's Encrypt's ACME service to obtain trusted certificates. + - If `caddy_ca_pem` and `caddy_ca_key` are provided, Caddy will use these external CA credentials for TLS. +- Ensure the Caddy service is running and enabled. + +# Requirements + +- A DNS A record resolving `caddy_domain` to the target host's IP address is recommended for proper certificate validation. +- Ports 80 and 443 must be open on the target host and accessible for inbound connections. +- 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. + +# Dependencies + +None. + +# Parameters + +| Variable | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `caddy_domain` | `str` | `True` | | Domain name for the Caddy reverse proxy (e.g., `proxy.example.com`). | +| `caddy_www_root` | `path` | `False` | `/var/www_root` | Directory where static WWW service content will be served from. | +| `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. | +| `caddy_ca_pem` | `path` | `False` | | Path to an external CA certificate file (PEM format) to be used by Caddy for TLS. | +| `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. | + +# Example Playbook + +```yaml +- hosts: proxy_servers + tasks: + - name: Install Caddy with default self-signed TLS + ansible.builtin.import_role: + name: cloudera.exe.caddy + vars: + caddy_domain: "dev-proxy.example.com" + # caddy_self_signed will default to true + # caddy_www_root will default to /var/www_root + + - name: Install Caddy using Let's Encrypt + ansible.builtin.import_role: + name: cloudera.exe.caddy + vars: + caddy_domain: "prod-proxy.example.com" + caddy_self_signed: false # Enable Let's Encrypt ACME + + - name: Install Caddy with external CA certificates + ansible.builtin.import_role: + name: cloudera.exe.caddy + vars: + caddy_domain: "internal-proxy.example.com" + caddy_self_signed: true # Still technically self-signed, but by an external CA + caddy_ca_pem: "/path/to/my_org_ca.pem" + caddy_ca_key: "/path/to/my_org_ca.key" + caddy_www_root: "/srv/my_app_html" +``` + +# License + +``` +Copyright 2025 Cloudera, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/roles/caddy/defaults/main.yml b/roles/caddy/defaults/main.yml new file mode 100644 index 00000000..bf1bd5e0 --- /dev/null +++ b/roles/caddy/defaults/main.yml @@ -0,0 +1,21 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- + +caddy_self_signed: true +caddy_domain: "{{ undef(hint='Please provide the default domain for the Caddy service') }}" +caddy_www_root: /var/www_root +# caddy_ca_pem: # path +# caddy_ca_key: # path diff --git a/roles/caddy/handlers/main.yml b/roles/caddy/handlers/main.yml new file mode 100644 index 00000000..c00513b6 --- /dev/null +++ b/roles/caddy/handlers/main.yml @@ -0,0 +1,20 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- + +- name: Reload Caddy + ansible.builtin.service: + name: "{{ caddy_service }}" + state: reloaded diff --git a/roles/caddy/meta/argument_specs.yml b/roles/caddy/meta/argument_specs.yml new file mode 100644 index 00000000..130c76c1 --- /dev/null +++ b/roles/caddy/meta/argument_specs.yml @@ -0,0 +1,54 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +argument_specs: + main: + short_description: Install Caddy proxy packages. + description: + - Install Caddy proxy service using self-signed TLS. + - Configure an initial global configuration with an import directory. + - Configure a WWW root directory. + - Optionally, provision the Caddy with external CA certificates. + - 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. + author: Cloudera Labs + version_added: "5.0.0" + options: + caddy_domain: + description: Domain name for the Caddy reverse proxy. + type: str + required: true + caddy_www_root: + description: Directory of the static WWW service. + type: path + required: false + default: /var/www_root + caddy_self_signed: + description: + - Flag enabling Caddy to issue self-signed TLS certificates. + - If not defined, Caddy will default to the Let's Encrypt ACME service. + - If defined and O(caddy_ca_pem) is not defined, Caddy will generate a root CA certificate for self-signed TLS. + type: bool + required: false + default: true + caddy_ca_pem: + description: + - External CA for the Caddy TLS service. + type: path + required: false + caddy_ca_key: + description: + - External CA key for the Caddy TLS service. + - Required if O(caddy_ca_pem) is defined. + type: path + required: false diff --git a/roles/caddy/tasks/RedHat-pre.yml b/roles/caddy/tasks/RedHat-pre.yml new file mode 100644 index 00000000..f1929724 --- /dev/null +++ b/roles/caddy/tasks/RedHat-pre.yml @@ -0,0 +1,23 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- + +- name: Enable Fedora COPR + ansible.builtin.package: + name: "dnf-command(copr)" + +- name: Enable Caddy project in COPR + ansible.builtin.command: "dnf copr enable -y @caddy/caddy" + changed_when: false diff --git a/roles/caddy/tasks/default-pre.yml b/roles/caddy/tasks/default-pre.yml new file mode 100644 index 00000000..30cf75a8 --- /dev/null +++ b/roles/caddy/tasks/default-pre.yml @@ -0,0 +1,17 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- + +# no op diff --git a/roles/caddy/tasks/main.yml b/roles/caddy/tasks/main.yml new file mode 100644 index 00000000..7ddcd15b --- /dev/null +++ b/roles/caddy/tasks/main.yml @@ -0,0 +1,142 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- + +- name: Gather host distribution details + ansible.builtin.setup: + gather_subset: distribution + +- name: Load distribution variables + ansible.builtin.include_vars: "{{ item }}" + with_first_found: + - "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}.yml" + - "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml" + - "{{ ansible_facts['distribution'] }}.yml" + - "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_version'] }}.yml" + - "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_major_version'] }}.yml" + - "{{ ansible_facts['os_family'] }}.yml" + - "default.yml" + +- name: Run distribution pre-tasks + ansible.builtin.include_tasks: "{{ item }}" + with_first_found: + - "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}-pre.yml" + - "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}-pre.yml" + - "{{ ansible_facts['distribution'] }}-pre.yml" + - "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_version'] }}-pre.yml" + - "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_major_version'] }}-pre.yml" + - "{{ ansible_facts['os_family'] }}-pre.yml" + - "default-pre.yml" + +- name: Install Caddy binaries + ansible.builtin.package: + name: "{{ caddy_package }}" + loop: "{{ caddy_packages }}" + loop_control: + loop_var: caddy_package + +- name: Set up Caddyfile imports directory + ansible.builtin.file: + path: "/etc/caddy/Caddyfile.d" + mode: "0755" + state: directory + +- name: Set up Caddy WWW root directory + ansible.builtin.file: + path: "{{ caddy_www_root }}" + mode: "0755" + state: directory + +- name: Set up default index page for Caddy WWW root + ansible.builtin.file: + path: "{{ [caddy_www_root, 'index.html'] | path_join }}" + mode: "0755" + state: touch + +- name: Provision external CA certificates + when: caddy_ca_pem is defined + block: + - name: Set up external PKI directory + ansible.builtin.file: + path: "{{ caddy_external_pki_dir }}" + state: directory + owner: caddy + group: caddy + mode: "0700" + + - name: Install external CA certificates + ansible.builtin.copy: + src: "{{ __ca_file }}" + dest: "{{ [caddy_external_pki_dir, __ca_file | basename] | path_join }}" + owner: caddy + group: caddy + mode: "0600" + loop: + - "{{ caddy_ca_pem }}" + - "{{ caddy_ca_key }}" + loop_control: + loop_var: __ca_file + +# - name: Set up Caddyfile ACME issuers +# ansible.builtin.blockinfile: +# backup: no +# path: "/etc/caddy/Caddyfile" +# insertbefore: BOF +# # append_newline: yes <=2.16 +# block: | +# { +# cert_issuer acme +# cert_issuer acme { +# dir https://acme.zerossl.com/v2/DV90 +# email some@email.goes.here +# } +# } + +# - name: Set up Caddy CA +# when: caddy_self_signed +# ansible.builtin.blockinfile: +# backup: no +# path: "/etc/caddy/Caddyfile" +# insertbefore: BOF +# # append_newline: yes <=2.16 +# block: "{{ lookup('template', 'internal_ca.json.j2') }}" + +# - name: Set up Caddyfile imports directive +# ansible.builtin.blockinfile: +# backup: no +# path: "/etc/caddy/Caddyfile" +# insertafter: EOF +# # prepend_newline: yes <=2.16 +# block: | +# import Caddyfile.d/*.caddyfile + +- name: Provision Caddy configuration + ansible.builtin.template: + src: Caddyfile.j2 + dest: /etc/caddy/Caddyfile + mode: "0755" + +- name: Enable and run the Caddy service + ansible.builtin.service: + name: "{{ caddy_service }}" + enabled: true + state: started + +- name: Retrieve the Caddy self-signed CA certificate + when: caddy_self_signed and caddy_ca_pem is undefined + ansible.builtin.fetch: + src: /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt + dest: "{{ [playbook_dir, name_prefix + '-caddy-root.crt'] | path_join }}" + flat: true diff --git a/roles/caddy/templates/Caddyfile.j2 b/roles/caddy/templates/Caddyfile.j2 new file mode 100644 index 00000000..80b771cf --- /dev/null +++ b/roles/caddy/templates/Caddyfile.j2 @@ -0,0 +1,26 @@ +# Managed by cloudera.exe.caddy + +{% if caddy_self_signed %} +{ + local_certs +{% if caddy_ca_pem is defined %} + pki { + ca { + root { + cert {{ [caddy_external_pki_dir, caddy_ca_pem | basename] | path_join }} + key {{ [caddy_external_pki_dir, caddy_ca_key | basename] | path_join }} + } + } + } +{% endif %} +} +{% endif %} + +# Caddy default WWW root +{{ caddy_domain }} { + root * {{ caddy_www_root }} + file_server +} + +# Caddy routes +import Caddyfile.d/*.caddyfile diff --git a/roles/caddy/vars/RedHat.yml b/roles/caddy/vars/RedHat.yml new file mode 100644 index 00000000..cb834692 --- /dev/null +++ b/roles/caddy/vars/RedHat.yml @@ -0,0 +1,18 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +caddy_packages: + - caddy + +caddy_service: caddy diff --git a/roles/caddy/vars/default.yml b/roles/caddy/vars/default.yml new file mode 100644 index 00000000..cb834692 --- /dev/null +++ b/roles/caddy/vars/default.yml @@ -0,0 +1,18 @@ +# Copyright 2025 Cloudera, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +caddy_packages: + - caddy + +caddy_service: caddy