From 67fb503a710712260f67407e705bd981866c24d8 Mon Sep 17 00:00:00 2001 From: Monviech <79600909+Monviech@users.noreply.github.com> Date: Fri, 8 Mar 2024 12:52:21 +0100 Subject: [PATCH] www/caddy: include os-caddy into OPNsense plugins (#3840) --- www/caddy/Makefile | 8 + www/caddy/README.md | 205 +++++++ www/caddy/pkg-descr | 40 ++ www/caddy/src/etc/inc/plugins.inc.d/caddy.inc | 64 ++ www/caddy/src/etc/syslog-ng.conf.d/caddy.conf | 48 ++ .../OPNsense/Caddy/Api/GeneralController.php | 41 ++ .../Caddy/Api/ReverseProxyController.php | 215 +++++++ .../OPNsense/Caddy/Api/ServiceController.php | 67 ++ .../OPNsense/Caddy/GeneralController.php | 47 ++ .../OPNsense/Caddy/ReverseProxyController.php | 45 ++ .../OPNsense/Caddy/forms/dialogAccessList.xml | 28 + .../OPNsense/Caddy/forms/dialogBasicAuth.xml | 20 + .../OPNsense/Caddy/forms/dialogHandle.xml | 91 +++ .../Caddy/forms/dialogReverseProxy.xml | 71 +++ .../OPNsense/Caddy/forms/dialogSubdomain.xml | 57 ++ .../OPNsense/Caddy/forms/dnsprovider.xml | 63 ++ .../OPNsense/Caddy/forms/dynamicdns.xml | 34 ++ .../OPNsense/Caddy/forms/general.xml | 33 + .../OPNsense/Caddy/forms/logsettings.xml | 23 + .../mvc/app/models/OPNsense/Caddy/ACL/ACL.xml | 18 + .../mvc/app/models/OPNsense/Caddy/Caddy.php | 160 +++++ .../mvc/app/models/OPNsense/Caddy/Caddy.xml | 330 ++++++++++ .../app/models/OPNsense/Caddy/Menu/Menu.xml | 9 + .../OPNsense/Caddy/Migrations/M1_1_3.php | 80 +++ .../mvc/app/views/OPNsense/Caddy/general.volt | 194 ++++++ .../views/OPNsense/Caddy/reverse_proxy.volt | 351 +++++++++++ .../scripts/OPNsense/Caddy/caddy_certs.php | 86 +++ .../scripts/OPNsense/Caddy/caddy_control.py | 80 +++ .../opnsense/scripts/OPNsense/Caddy/setup.sh | 38 ++ .../service/conf/actions.d/actions_caddy.conf | 29 + .../service/templates/OPNsense/Caddy/+TARGETS | 2 + .../templates/OPNsense/Caddy/Caddyfile | 573 ++++++++++++++++++ .../templates/OPNsense/Caddy/rc.conf.d/caddy | 13 + 33 files changed, 3163 insertions(+) create mode 100644 www/caddy/Makefile create mode 100644 www/caddy/README.md create mode 100644 www/caddy/pkg-descr create mode 100644 www/caddy/src/etc/inc/plugins.inc.d/caddy.inc create mode 100644 www/caddy/src/etc/syslog-ng.conf.d/caddy.conf create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/GeneralController.php create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ServiceController.php create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/GeneralController.php create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dnsprovider.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dynamicdns.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/logsettings.xml create mode 100644 www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/ACL/ACL.xml create mode 100644 www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php create mode 100644 www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml create mode 100644 www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Menu/Menu.xml create mode 100644 www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Migrations/M1_1_3.php create mode 100644 www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt create mode 100644 www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt create mode 100755 www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php create mode 100755 www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py create mode 100755 www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh create mode 100644 www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf create mode 100644 www/caddy/src/opnsense/service/templates/OPNsense/Caddy/+TARGETS create mode 100644 www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile create mode 100644 www/caddy/src/opnsense/service/templates/OPNsense/Caddy/rc.conf.d/caddy diff --git a/www/caddy/Makefile b/www/caddy/Makefile new file mode 100644 index 000000000..31f2035bd --- /dev/null +++ b/www/caddy/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= caddy +PLUGIN_VERSION= 1.5.1 +PLUGIN_REVISION= 2 +PLUGIN_DEPENDS= caddy-custom +PLUGIN_COMMENT= Easy to configure Reverse Proxy based on Caddy with Automatic HTTPS and Dynamic DNS +PLUGIN_MAINTAINER= cedrik@pischem.com + +.include "../../Mk/plugins.mk" diff --git a/www/caddy/README.md b/www/caddy/README.md new file mode 100644 index 000000000..a1ba325cb --- /dev/null +++ b/www/caddy/README.md @@ -0,0 +1,205 @@ +# Caddy Plugin for OPNsense + +- This project provides a simple yet powerful plugin for [OPNsense](https://github.com/opnsense) to enable support for [Caddy](https://github.com/caddyserver/caddy). +- The scope is the reverse proxy features. +- The main goal is an easy to configure plugin. Most options that aren't generally needed are hidden behind the advanced mode for this reason. +- The feature set is complete for now. + +## Main Features + +- Modern and fast Reverse Proxy based on [Caddy](https://caddyserver.com/) +- Automatic Let's Encrypt and ZeroSSL Certificates without configuration with HTTP-01 and TLS-ALPN-01 +- ACME DNS-01 challenge with configuration (requires supported DNS Provider) +- Dynamic DNS (DynDns) with configuration (requires supported DNS Provider) +- Supported DNS Providers in GUI: ```cloudflare, duckdns, digitalocean, dnspod, hetzner, godaddy, gandi, ionos, desec, porkbun, route53, acmedns, alidns, googleclouddns, azure, openstack-designate, ovh, namecheap, netlify, namesilo, powerdns, vercel, ddnss, njalla, metaname, linode, tencentcloud, dinahosting, hexonet, mailinabox``` +- Use custom certificates from OPNsense certificate store +- Normal domains, wildcard domains and subdomains +- Access Lists to restrict access based on static networks +- Basic Auth to restrict access by username and password +- Syslog-ng integration and HTTP Access Log +- NTLM Transport for Exchange Server + +## License + +- This project is licensed under the BSD 2-Clause "Simplified" license. See the LICENSE file for details. +- Caddy is licensed under the Apache License, Version 2.0. +- OPNsense is licensed under the BSD 2-Clause “Simplified” license. + +## Acknowledgments + +- Thanks to the Caddy community/developers for creating a fantastic open source web server. +- Thanks to the OPNsense community/developers for creating a powerful and flexible open source firewall and routing platform. +- Additional big **Thank You** in no particular order: [AdShellevis](https://github.com/Adschellevis), [mimugmail](https://forum.opnsense.org/index.php?action=profile;u=15464), [gspannu](https://github.com/gspannu), [francislavoie](https://caddy.community/u/francislavoie/summary), [matt](https://caddy.community/u/matt/summary), [fichtner](https://github.com/fichtner) + +# How to install + +- Install "os-caddy" from the OPNsense Plugins. + +## Prepare Caddy for use after the installation + +**Attention**, additional preparation of OPNsense needed: +- Make sure that port `80` and `443` aren't occupied. You have to change the default listen port to `8443` for example. Go to `System: Settings: Administration` to change the `TCP Port`. Then also enable `HTTP Redirect - Disable web GUI redirect rule`. +- If you have other reverse proxy or webserver plugins installed, make sure they don't use the same ports as Caddy +- Create Firewall rules that allow 80 and 443 TCP to "This Firewall" on WAN and (optionally) LAN, OPT1 etc... +- There is a lot of input validation. If you read all the hints, help texts and error messages, its unlikely that you create a configuration that won't work. +- **Attention**: If you use this in HA (High Availability), only use your own custom certificates. Caddy needs a shared storage for the ACME challenges to work on two or more firewalls in HA at the same time. This is out of scope, since offering shared storage on firewalls where one can potentially fail, would leave the other without storage for Caddy to work with. + +# Available Settings in "Services - Caddy Web Server" +**Please note that some options are hidden in advanced mode.** +## General Settings - General +- `Enable` or `disable` Caddy +- `ACME Email`: e.g. `info@example.com`, it's optional. +- `Auto HTTPS`: `On (default)` creates automatic Let's Encrypt Certificates for all Domains that don't have more specific options set, like custom certificates. +- `Trusted Proxies`: Leave empty if you don't use a CDN in front of your OPNsense. If you use Cloudflare or another CDN provider, create an access list with the IP addresses of that CDN and add it here. Add the same Access List to the domain this CDN tries to reach. +- `Abort Connections`: This option, when enabled, aborts all connections to the Reverse Proxy Domain that don't match any specified handler or access list. This setting doesn't affect Let's Encrypt's ability to issue certificates, ensuring secure connections regardless of the option's status. If unchecked, the Reverse Proxy Domain remains accessible even without a matching handler, allowing for connectivity and certificate checks, even in the absence of a configured Backend Server. When using Access Lists, enabling this option is recommended to reject unauthorized connections outright. Without this option, unmatched IP addresses will encounter an empty page instead of an explicit rejection, though the Access Lists continue to function and restrict access. + +## General Settings - DNS Provider +- `DNS Provider`: Select the DNS provider for the DNS-01 Challenge and Dynamic DNS. This is optional, since certificates will be requested from Let's Encrypt via HTTP-01 or TLS-ALPN-01 Challenge when this option is unset. You mostly need this for Wildcard Certificates, and for Dynamic DNS. To use the DNS-01 Challenge and Dynamic DNS, enable the checkbox in a Reverse Proxy Domain or Subdomain. For more information: https://github.com/caddy-dns +- `DNS API Standard Field`: This is the standard field for the API Key. Field can be left empty if optional: Cloudflare "api_token", Duckdns "api_token", DigitalOcean "auth_token", DNSPod "auth_token", Hetzner "api_token", Godaddy "api_token", Gandi "bearer_token", IONOS "api_token", deSEC "token", Route53 "access_key_id", Porkbun "api_key", ACME-DNS "username", Netlify "personal_access_token", Namesilo "api_token", Njalla "api_token", Vercel "api_token", Google Cloud DNS "gcp_project", Alidns "access_key_id", Azure "tenant_id", OpenStack Designate "region_name", OVH "endpoint", Namecheap "api_key", PowerDNS "server_url", DDNSS "api_token", Metaname "api_key", Linode "api_token", Tencent Cloud "secret_id", Dinahosting "username", Hexonet "username", Mail-in-a-Box "api_url". +- `DNS API Additional Field 1`: Leave empty if your DNS Provider isn't specified here. Field can be left empty if optional: Duckdns "override_domain", Route53 "secret_access_key", Porkbun "api_secret_key", ACME-DNS "password", Alidns "access_key_secret", Azure "client_id", OpenStack Designate "tenant_id", OVH "application_key", Namecheap "user", PowerDNS "api_token", DDNSS "username", Metaname "account_reference", Linode "api_url", Tencent Cloud "secret_key", Dinahosting "password", Hexonet "password", Mail-in-a-Box "email_address". +- `DNS API Additional Field 2`: Leave empty if your DNS Provider isn't specified here. Field can be left empty if optional: Route53 "max_retries", ACME-DNS "subdomain", Azure "client_secret", OpenStack Designate "identity_api_version", OVH "application_secret", Namecheap "api_endpoint", DDNSS "password", Linode "api_version", Mail-in-a-Box "password". +- `DNS API Additional Field 3`: Leave empty if your DNS Provider isn't specified here. Field can be left empty if optional: Route53 "aws_profile", ACME-DNS "server_url", Azure "subscription_id", OpenStack Designate "password", OVH "consumer_key", Namecheap "client_ip", DDNS "password". +- `DNS API Additional Field 4`: Leave empty if your DNS Provider isn't specified here. Field can be left empty if optional: Route53 "region", Azure "resource_group_name", OpenStack Designate "username". +- `DNS API Additional Field 5`: Leave empty if your DNS Provider isn't specified here. Field can be left empty if optional: Route53 "token", OpenStack Designate "tenant_name". +- `DNS API Additional Field 6`: Leave empty if your DNS Provider isn't specified here. Field can be left empty if optional: OpenStack Designate "auth_url". +- `DNS API Additional Field 7`: Leave empty if your DNS Provider isn't specified here. Field can be left empty if optional: OpenStack Designate "endpoint_type". + +## General Settings - Dynamic DNS +- `DynDns Check Http`: Optionally, enter an URL to test the current IP address of the firewall via HTTP procotol. Generally, this is not needed. Caddy uses default providers to test the current IP addresses. If you rather use your own, enter the https:// link to an IP address testing website. +- `DynDns Check Interface`: Optionally, select an interface to extract the current IP address of the firewall. Attention, all IP addresses will be read from this interface. Only choose this option if you know the implications. +- `DynDns Check Interval`: Interval to poll for changes of the IP address. The default is 5 minutes. Can be a number between 1 to 1440 minutes. +- `DynDns IP Version`: Leave on None to set IPv4 A-Records and IPv6 AAAA-Records. Select "Ipv4 only" for setting A-Records. Select "IPv6 only" for setting AAAA-Records. +- `DynDns TTL`: Set the TTL (time to live) for DNS Records. The default is 1 hour. Can be a number between 1 to 24 hours. + +## General Settings - Log Settings +- `Log Credentials`: Log all Cookies and Authorization in HTTP request logging. Use combined with HTTP Access Log in the Reverse Proxy Domain. Enable this option only for troubleshooting. +- `Log Access in Plain Format`: Don't send HTTP(S) access logs to the central OPNsense logging facility but save them in plain Caddy JSON format in a subdirectory instead. Only effective for Reverse Proxy Domains that have HTTP Access Log enabled. The feature is intended to have access log files processed by e.g. CrowdSec. They can be found in `/var/log/caddy/access`. +- `Keep Plain Access Logs for (days)`: How many days until the plain format log files are deleted. + +## Reverse Proxy - Domains +- Press `+` to create a new Reverse Proxy Domain +- `Enable` this new entry +- `Reverse Proxy Domain`: Can either be a domain name or an IP address. If a domain name is chosen, Caddy will automatically try to get an ACME certificate, and the header will be automatically passed to the Server in the backend. +- `Reverse Proxy Port`: Should be the port the OPNsense will listen on. Don't forget to create Firewall rules that allow traffic to this port on `WAN` or `LAN` to `This Firewall`. You can leave this empty if you want to use the default ports of Caddy (`80` and `443`) with automatic redirection from HTTP to HTTPS. +- `Access List`: Restrict the access to this domain to a list of IP addresses you define in the `Access` Tab. This doesn't influence the Let's Encrypt certificate generation, so you can be as restrictive as you want here. +- `Basic Auth`: Restrict the access to this domain to one or multiple users you define in the `Access` Tab. This doesn't influence the Let's Encrypt certificate generation, so you can be as restrictive as you want here. +- `DNS-01 challenge`: Enable this if you want to use the `DNS-01` ACME challenge instead of HTTP challenge. This can be set per entry, so you can have both types of challenges at the same time for different entries. This option needs the `General Settings` - `DNS Provider` and `API KEY` set. +- `Dynamic DNS`: Enable Dynamic DNS, please configure DNS Provider and API Key in General Settings. The DNS Records of this domain will be automatically updated with your DNS Provider. +- `Custom Certificate`: Use a Certificate you imported or generated in `System - Trust - Certificates`. The chain is generated automatically. `Certificate + Intermediate CA + Root CA`, `Certificate + Root CA` and `self signed Certificate` are all fully supported. +- `HTTP Access Log`: Enable the HTTP request logging for this domain and its subdomains. This option is mostly for troubleshooting since it will log every single request. +- `Description`: The description is mandatory. Create descriptions for each domain. Since there could be multiples of the same domain with different ports, do it like this: `foo.example.com` and `foo.example.com.8443`. + +## Reverse Proxy - Subdomains +- Press `+` to create a new Reverse Proxy Subdomain +- `Reverse Proxy Domain` - Choose a wildcard domain you prepared in "Reverse Proxy - Domains", it has to be formatted like `*.example.com` +- `Reverse Proxy Subdomain` - Create a name that is seated under the Wildcard domain, for example `foo.example.com` and `bar.example.com`. +- For the other options refer to Domains. + +## Reverse Proxy - Handler +Please note that the order that handlers are saved in the scope of each domain or domain/subdomain can influence functionality - The first matching handler wins. So if you put /ui* in front of a more specific handler like /ui/opnsense, the /ui* will match first and /ui/opnsense won't ever match (in the scope of their domain). Right now there isn't an easy way to move the position of handlers in the grid, so you have to clone them if you want to change their order, and delete the old entries afterwards. Most of the time, creating just one empty catch-all handler is the best choice. The template logic makes sure that catch-all handlers are always placed last, after all other handlers. +- Press `+` to create a new `Handler`. A Handler is like a location in nginx. +- `Enable` this new entry. +- `Reverse Proxy Domain`: Select the domain you have created in `Reverse Proxy Domains`. +- `Reverse Proxy Subdomain`: Leave this on `None`. It is not needed without having a wildcard certificate, or a `*.example.com` Domain. +- `Handle Type`: `Handle` or `Handle Path` can be chosen. If in doubt, always use `Handle`, the most common option. `Handle Path` is used to strip the path from the URI. For example if you have example.com/opnsense internally, but want to call it with just example.com externally. +- `Handle Path`: Leave this empty if you want to create a catch all location. You can create multiple Handler entries, and have each of them point at different locations like `/foo/*` or `/foo/bar/*` or `/foo*`. +- `Backend Server Domain`: Should be an internal domain name or an IP Address of the Backend Server that should receive the traffic of the `Reverse Proxy Domain`. +- `Backend Server Port`: Should be the port the Backend Server listens on. This can be left empty to use Caddy default ports 80 and 443. +- `Backend Server Path`: In case the backend application resides in a sub-path of the web root and you don't want this path visible in the frontend URL you can use this setting to prepend an initial path starting with '/' to every backend request. Java applications running in a servlet container like Tomcat are known to behave this way, so you can set it to e.g. '/guacamole' to access Apache Guacamole at the frontend root URL without needing a redirect. +- `TLS`: If your Backend Server only accepts HTTPS, enable this option. If the Backend Server has a globally trusted certificate, this is all you need. +- `TLS Trusted CA Certificates`: Choose a CA certificate to trust for the Backend Server connection. Import your self-signed certificate or your CA certificate into the OPNsense "System - Trust - Authorities" store, and select it here. +- `TLS Server Name`: If the SAN (Subject Alternative Names) of the offered trusted CA certificate or self-signed certificate doesn't match with the IP address or hostname of the `Backend Server Domain`, you can enter it here. This will change the SNI (Server Name Identification) of Caddy to the `TLS Server Name`. IP address e.g. `192.168.1.1` or hostname e.g. `localhost` or `opnsense.local` are all valid choices. Only if the SAN and SNI match, the TLS connection will work, otherwise an error is logged that can be used to troubleshoot. +- `NTLM`: If your Backend Server needs NTLM authentication, enable this option together with `TLS`. For example, Exchange Server. + +**Attention**: The GUI doesn't allow "tls_insecure_skip_verify" due to safety reasons, as the Caddy documentation states not to use it. Use the `TLS Trusted CA Certificates` and `TLS Server Name` options instead to get a **secure TLS connection** to your Backend Server. Otherwise, use HTTP. If you really need to use "tls_insecure_skip_verify" and know the implications, use the import statements of custom configuration files. + +## Reverse Proxy - Access - Access Lists +- Press `+` to create a new Access List +- `Access List name`: Choose a name for the Access List, for example `private_ips`. +- `Client IP Addresses`: Enter any number of IPv4 and IPv6 addresses or networks that this access list should contain. For example for matching only internal networks, add `192.168.0.0/16` `172.16.0.0/12` `10.0.0.0/8` `127.0.0.1/8` `fd00::/8` `::1`. +- `Invert List`: Invert the logic of the access list. If unchecked, the Client IP Addresses will be ALLOWED, all other IP addresses will be blocked. When checked, the Client IP Addresses will be BLOCKED, all other IP addresses will be allowed. +- Afterwards, go back to Domains or Subdomains and add the Access List you have created to them (advanced mode). All handlers created under these Domains will get an additional matcher. That means, the requests still reach Caddy, but if the IP Addresses don't match with the Access List logic, the request doesn't match any handler and will be dropped before being reverse proxied to any Backend Server. If you are using a CDN, make sure the Access List in General - Trusted Proxies and on each Domain used for that CDN are the same. + +## Reverse Proxy - Access - Basic Auth +- Press `+` to create a new User for Basic Auth +- `User`: Enter a username. Afterwards, you can select it in Reverse Proxy Domains or Subdomains to restrict access with basic auth. Usernames are only allowed to have alphanumeric characters. +- `Password`: Enter a password. Write it down. It will be hashed with bcrypt. It can only be set and changed but won't be visible anymore. The hash can't be turned back into the original password. +- Afterwards, go back to Domains or Subdomains and add the one or multiple basic auth users you have created to them (advanced mode). The basic auth matches after access lists, so you can set both to first restrict access by IP address, and then additionally by username and password. Please note that if you delete a user before deselecting it in a domain, the basic auth will stay with no user. If that happens you have to select the "clear all" in the domain or subdomain and save. Don't set basic auth on top of a wildcard domain directly, always set it on the subdomains instead. + +# HOW TO Section: + +## HOW TO: Create an easy reverse proxy +**Services - Caddy Web Server - General Settings:** +- `Enable` Caddy and press `Apply` + +**Services - Caddy Web Server - Reverse Proxy - Domain:** +- Press `+` to create a new Reverse Proxy Domain +- `Reverse Proxy Domain` - `foo.example.com` +- `Description` - `foo.example.com` +- `Save` + +**Services - Caddy Web Server - Reverse Proxy - Handler:** +- Press `+` to create a new Handler +- `Reverse Proxy Domain` - `foo.example.com` +- `Backend Server Domain` - `192.168.10.1` +- `Save` +- `Apply` + +Done, leave all other fields to default or empty. You don't need the advanced mode options. After just a few seconds the Let's Encrypt Certificate will be installed and everything just works. Check the Logfile for that. +Now you have a "Internet <-- HTTPS --> OPNsense (Caddy) <-- HTTP --> Backend Server" Reverse Proxy. + +## HOW TO: Create a wildcard subdomain reverse proxy +- Do everything the same as above, but create your Reverse Proxy Domain like this `*.example.com` and activate the `DNS-01` challenge checkbox. +- OR - `Custom Certificate` - Use a Certificate you imported or generated in `System - Trust - Certificates`. It has to be a wildcard certificate. +- Go to the `Reverse Proxy Subdomain` Tab and create all subdomains that you need in relation to the `*.example.com` domain. So for example `foo.example.com` and `bar.example.com`. +- Create descriptions for each subdomain. Since there could be multiples of the same subdomain with different ports, do it like this: `foo.example.com` and `foo.example.com.8443`. +- In the `Handler` Tab you can now select your `*.example.com` `Reverse Proxy Domain`, and if `Reverse Proxy Subdomain` is `None`, the Handlers are added to the base `Reverse Proxy Domain`. For example, if you want a catch all Handler for all non referenced subdomains. +- If you create a Handler with `*.example.com` as `Reverse Proxy Domain` and `foo.example.com` as `Reverse Proxy Subdomain`, a nested Handler will be generated. You can do all the same configurations as if the subdomain is a normal domain, with multiple Handlers and Handler paths. + +## HOW TO: Create a Handle with TLS and a trusted self-signed Certificate +**Example: Reverse Proxy the OPNsense Configuration GUI Website with Caddy** +- Open your OPNsense GUI in a Browser (e.g. Chrome or Firefox). Inspect the certificate. Copy the SAN for later use, for example `OPNsense.localdomain`. +- Save the certificate in your Browser as PEM file. Open it up with a text editor, and copy the contents into a new entry in `System - Trust - Authorities`. Name the certificate e.g. `opnsense-selfsigned`. +- Add a new Reverse Proxy Domain, for example `opn.example.com`. Make sure the name is externally resolvable to the IP of your OPNsense Firewall with Caddy. +- Add a new Handler with the following options (enable advanced mode): +- `Reverse Proxy Domain`: `opn.example.com` +- `Backend Server Domain`: `127.0.0.1` +- `Backend Server Port`: `8443` (Enter the port of your OPNsense GUI. You have changed it from 443 to a different port, since Caddy needs port 443.) +- `TLS`: `X` +- `TLS Trusted CA Certificates`: `opnsense-selfsigned` (The certificate you have saved in `System - Trust - Authorities`) +- `TLS Server Name`: `OPNsense.localdomain` (The SAN of the certificate) +- Save +- Apply +- Open `https://opn.example.com` and it should serve the reverse proxied OPNsense Configuration GUI Website. Check the log file for errors if it doesn't work, most of the time the `TLS Server Name` doesn't match the SAN of the `TLS Trusted CA Certificates`. Please note that Caddy doesn't support CN (Common Name) in certificate since it's been deprecated since many years. +- Additionally, you can create an access list to limit access to the GUI only from trusted IP addresses (recommended). Add that access list to the domain `opn.example.com` in advanced mode. Also, enable `Abort Connections` in the `General` Settings to abort all connections immediately that don't match the access list or the handler. + +# Troubleshooting +- You can always test if your current Caddyfile is valid by invoking `/api/caddy/service/validate` - This is also done automatically each time `Apply` is pressed. If you have an invalid configuration, Caddy will refuse to start and show the exact error message. +- Check `/var/log/caddy/caddy.log` or `@latest.log` to find errors. There is also a Caddy Log File in the GUI. +- A good indicator that Caddy is indeed running is this log entry: `serving initial configuration` +- Check the Service Widget and the "General Settings" Service Control buttons. If everything works they should show a green "Play" sign. If Caddy is stopped there is a red "Stop" sign. If Caddy is disabled, there is no widget and no control buttons. + +# Build caddy and os-caddy from source +- As build system use a FreeBSD 13.2 - https://github.com/opnsense/tools +- Use xcaddy to build your own caddy binary. Additonal Caddy plugins can be compiled in, here is an example: [Additional Plugins](https://github.com/opnsense/tools/blob/a555d25b11486835460a136af0b8ad2e517ae96b/config/24.1/make.conf#L94) +- Check the +MANIFEST file and put all dependant files into the right paths on your build system. Make sure to check your own file hashes with ```sha256 /path/to/file```. +- Use ```pkg create -M ./+MANIFEST``` in the folder of the ```+MANIFEST``` file. +- For os-caddy.pkg make sure you have the OPNsense tools build system properly set up. +- Build the os-caddy.pkg by going into /usr/plugins/devel/caddy/ and invoking ```make package``` + +# Custom configuration files +- The Caddyfile has an additional import from the path ```/usr/local/etc/caddy/caddy.d/```. You can place your own custom configuration files inside that adhere to the Caddyfile syntax. +- ```*.global``` will be imported into the global block of the Caddyfile. Global options can be found here: [Global Options Block](https://caddyserver.com/docs/caddyfile/options) +- ```*.conf``` will be imported at the end of the Caddyfile, you can put your own reverse_proxy or other settings there. Don't forget to test your custom configuration with `caddy run --config /usr/local/etc/caddy/Caddyfile`. + +# Using the REST API to control the plugin: +The Rest API is now fully integreated with the OPNsense syntax. +https://docs.opnsense.org/development/api.html + +All API Actions can be found in the API Controller files ```/usr/local/opnsense/mvc/app/controllers/Pischem/Caddy/Api``` + +Examples: +- /api/caddy/ReverseProxy/get +- /api/caddy/General/get +- /api/caddy/service/status +- /api/caddy/service/validate diff --git a/www/caddy/pkg-descr b/www/caddy/pkg-descr new file mode 100644 index 000000000..f06e6c431 --- /dev/null +++ b/www/caddy/pkg-descr @@ -0,0 +1,40 @@ +Caddy - The Ultimate Server - makes your sites more secure, more reliable, and more scalable than any other solution. +By default, Caddy automatically obtains and renews TLS certificates for all your sites. +It's the most advanced HTTPS server in the world. + +Reverse Proxy HTTP, HTTPS, FastCGI, WebSockets, gRPC, FastCGI (usually PHP), and more! + +WWW: https://caddyserver.com/ + +Main features of this plugin: + +* Easy to configure and reliable! Reverse Proxy any HTTP/HTTPS or WebSocket application in minutes. +* Hard to break! Extensive validations of the configuration on each save and apply. +* Automatic Let's Encrypt and ZeroSSL Certificates with HTTP-01 and TLS-ALPN-01 challenge +* DNS-01 challenge and Dynamic DNS with supported DNS Providers built right in +* Use custom certificates from OPNsense certificate store +* Wildcard Domain and Subdomain support +* Access Lists to restrict access based on static networks +* Basic Auth to restrict access by username and password +* Syslog-ng integration and HTTP Access Log +* NTLM Transport + +Plugin Changelog +================ + +1.5.1 + +* More DNS Providers added: netlify, namesilo, njalla, vercel, googleclouddns, alidns, powerdns, tencentcloud, dinahosting, metaname, hexonet, ddnss, linode, mailinabox, ovh, namecheap, azure, openstack-designate. +* More input fields and better documentation added for the DNS Provider API Keys. +* Changed rc.d script to standard freebsd poudriere one packaged with the caddy-custom binary, included setup.sh script to rc.conf.d/caddy. +* Updated dependancy to caddy-custom instead of caddy. +* Removed +POST_DEINSTALL.post and +POST_INSTALL.post. +* Turned syslog-ng configuration from template to static file. +* A few typos in the general.volt and reverse_proxy.volt corrected. +* The RealInterfaceField custom Fieldtype was removed and replaced with an OPNsense integrated template function to read the interface name. +* Enable $internalModelUseSafeDelete in ReverseProxyController.php - Items can only be deleted when they are not referenced by other items, making deleting in the GUI safer since there can't be any orphaned configuration left behind. +* Migration script M1_1_3 from "Description" to "description" added. Lower case description is needed to be in line with some OPNsense integrated functions. + +1.5.0 + +* Initial release diff --git a/www/caddy/src/etc/inc/plugins.inc.d/caddy.inc b/www/caddy/src/etc/inc/plugins.inc.d/caddy.inc new file mode 100644 index 000000000..a04d784d9 --- /dev/null +++ b/www/caddy/src/etc/inc/plugins.inc.d/caddy.inc @@ -0,0 +1,64 @@ + gettext('Caddy Web Server'), + 'configd' => array( + 'restart' => array('caddy restart'), + 'start' => array('caddy start'), + 'stop' => array('caddy stop'), + ), + 'name' => 'caddy', + 'pidfile' => '/var/run/caddy/caddy.pid' + ); + } + + return $services; +} + +function caddy_xmlrpc_sync() +{ + $result = array(); + + $result[] = array( + 'description' => gettext('Caddy Web Server'), + 'section' => 'Pischem.caddy', + 'id' => 'caddy', + 'services' => ["caddy"], + ); + + return $result; +} diff --git a/www/caddy/src/etc/syslog-ng.conf.d/caddy.conf b/www/caddy/src/etc/syslog-ng.conf.d/caddy.conf new file mode 100644 index 000000000..11c96c45d --- /dev/null +++ b/www/caddy/src/etc/syslog-ng.conf.d/caddy.conf @@ -0,0 +1,48 @@ +################################################################### +# Local syslog-ng configuration [caddy]. +################################################################### +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +# +# Define Unix socket source for Caddy +source s_caddy { + unix-dgram("/var/caddy/var/run/log"); +}; + +# Parser for Caddy log levels +parser p_caddy_levels { + channel { + filter { + message(".*debug.*") or + message(".*info.*") or + message(".*warn.*") or + message(".*error.*") or + message(".*panic.*") or + message(".*fatal.*"); + }; + rewrite { + set-severity("7" condition(message(".*debug.*"))); # DEBUG -> Debug + set-severity("6" condition(message(".*info.*"))); # INFO -> Informational + set-severity("4" condition(message(".*warn.*"))); # WARN -> Warning + set-severity("3" condition(message(".*error.*"))); # ERROR -> Error + set-severity("2" condition(message(".*panic.*"))); # PANIC -> Critical + set-severity("1" condition(message(".*fatal.*"))); # FATAL -> Alert + }; + }; +}; + +# Destination for Caddy logs +destination d_local_caddy { + file( + "/var/log/caddy/caddy_${YEAR}${MONTH}${DAY}.log" + create-dirs(yes) + flags(syslog-protocol) + ); +}; + +# Log path for processing Caddy logs +log { + source(s_caddy); + parser(p_caddy_levels); + rewrite { set("caddy" value("PROGRAM")); }; + destination(d_local_caddy); +}; diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/GeneralController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/GeneralController.php new file mode 100644 index 000000000..21238afa1 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/GeneralController.php @@ -0,0 +1,41 @@ +searchBase("reverseproxy.reverse", ['enabled', 'FromDomain', 'FromPort', 'accesslist', 'basicauth', 'DnsChallenge', 'CustomCertificate', 'AccessLog', 'DynDns', 'description']); + } + + public function setReverseProxyAction($uuid) + { + return $this->setBase("reverse", "reverseproxy.reverse", $uuid); + } + + public function addReverseProxyAction() + { + return $this->addBase("reverse", "reverseproxy.reverse"); + } + + public function getReverseProxyAction($uuid = null) + { + return $this->getBase("reverse", "reverseproxy.reverse", $uuid); + } + + public function delReverseProxyAction($uuid) + { + return $this->delBase("reverseproxy.reverse", $uuid); + } + + public function toggleReverseProxyAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.reverse", $uuid, $enabled); + } + + + /*Subdomain Section*/ + + public function searchSubdomainAction() + { + return $this->searchBase("reverseproxy.subdomain", ['enabled', 'reverse', 'FromDomain', 'FromPort', 'accesslist', 'basicauth', 'DynDns', 'description']); + } + + public function setSubdomainAction($uuid) + { + return $this->setBase("subdomain", "reverseproxy.subdomain", $uuid); + } + + public function addSubdomainAction() + { + return $this->addBase("subdomain", "reverseproxy.subdomain"); + } + + public function getSubdomainAction($uuid = null) + { + return $this->getBase("subdomain", "reverseproxy.subdomain", $uuid); + } + + public function delSubdomainAction($uuid) + { + return $this->delBase("reverseproxy.subdomain", $uuid); + } + + public function toggleSubdomainAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.subdomain", $uuid, $enabled); + } + + + /*Handler Section*/ + + public function searchHandleAction() + { + return $this->searchBase("reverseproxy.handle", ['enabled', 'reverse', 'subdomain', 'HandleType', 'HandlePath', 'ToDomain', 'ToPort', 'ToPath', 'HttpTls', 'HttpTlsTrustedCaCerts', 'HttpTlsServerName', 'HttpNtlm', 'description']); + } + + public function setHandleAction($uuid) + { + return $this->setBase("handle", "reverseproxy.handle", $uuid); + } + + public function addHandleAction() + { + return $this->addBase("handle", "reverseproxy.handle"); + } + + public function getHandleAction($uuid = null) + { + return $this->getBase("handle", "reverseproxy.handle", $uuid); + } + + public function delHandleAction($uuid) + { + return $this->delBase("reverseproxy.handle", $uuid); + } + + public function toggleHandleAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.handle", $uuid, $enabled); + } + + + /* AccessList Section */ + + public function searchAccessListAction() + { + return $this->searchBase("reverseproxy.accesslist", ['accesslistName', 'clientIps', 'accesslistInvert', 'description']); + } + + public function setAccessListAction($uuid) + { + return $this->setBase("accesslist", "reverseproxy.accesslist", $uuid); + } + + public function addAccessListAction() + { + return $this->addBase("accesslist", "reverseproxy.accesslist"); + } + + public function getAccessListAction($uuid = null) + { + return $this->getBase("accesslist", "reverseproxy.accesslist", $uuid); + } + + public function delAccessListAction($uuid) + { + return $this->delBase("reverseproxy.accesslist", $uuid); + } + + + /* BasicAuth Section */ + + public function searchBasicAuthAction() + { + return $this->searchBase("reverseproxy.basicauth", ['basicauthuser', 'basicauthpass', 'description']); + } + + public function setBasicAuthAction($uuid) + { + if ($this->request->isPost()) { + $postData = $this->request->getPost(); + + if (isset($postData['basicauth']['basicauthpass']) && !empty(trim($postData['basicauth']['basicauthpass']))) { + $plainPassword = $postData['basicauth']['basicauthpass']; + $hashedPassword = password_hash($plainPassword, PASSWORD_BCRYPT); + $_POST['basicauth']['basicauthpass'] = $hashedPassword; + } + } + + return $this->setBase("basicauth", "reverseproxy.basicauth", $uuid); + } + + public function addBasicAuthAction() + { + if ($this->request->isPost()) { + $postData = $this->request->getPost(); + + if (isset($postData['basicauth']['basicauthpass']) && !empty(trim($postData['basicauth']['basicauthpass']))) { + $plainPassword = $postData['basicauth']['basicauthpass']; + $hashedPassword = password_hash($plainPassword, PASSWORD_BCRYPT); + $_POST['basicauth']['basicauthpass'] = $hashedPassword; + } + } + + return $this->addBase("basicauth", "reverseproxy.basicauth"); + } + + public function getBasicAuthAction($uuid = null) + { + return $this->getBase("basicauth", "reverseproxy.basicauth", $uuid); + } + + public function delBasicAuthAction($uuid) + { + return $this->delBase("reverseproxy.basicauth", $uuid); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ServiceController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ServiceController.php new file mode 100644 index 000000000..fa1b43842 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ServiceController.php @@ -0,0 +1,67 @@ +configdRun("template reload " . self::$internalServiceTemplate); + + // Validate the Caddyfile + $validateResult = trim($backend->configdRun('caddy validate')); + + // Attempt to parse the JSON output from the validation result + if (($jsonStartPos = strpos($validateResult, '{"message":')) !== false) { + $jsonOutput = substr($validateResult, $jsonStartPos); + $result = json_decode($jsonOutput, true); + + if (is_array($result) && isset($result['status'])) { + return ["status" => $result['status'], "message" => $result['message']]; + } + } + + // If unable to parse the expected JSON output, return a generic error message + return ["status" => "failed", "message" => "Unable to parse the validation result."]; + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/GeneralController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/GeneralController.php new file mode 100644 index 000000000..6d64473de --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/GeneralController.php @@ -0,0 +1,47 @@ +view->pick('OPNsense/Caddy/general'); + $this->view->generalForm = $this->getForm("general"); + $this->view->dnsproviderForm = $this->getForm("dnsprovider"); + $this->view->dynamicdnsForm = $this->getForm("dynamicdns"); + $this->view->logsettingsForm = $this->getForm("logsettings"); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php new file mode 100644 index 000000000..d6727b48d --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php @@ -0,0 +1,45 @@ +view->pick('OPNsense/Caddy/reverse_proxy'); + $this->view->formDialogReverseProxy = $this->getForm("dialogReverseProxy"); + $this->view->formDialogSubdomain = $this->getForm("dialogSubdomain"); + $this->view->formDialogHandle = $this->getForm("dialogHandle"); + $this->view->formDialogAccessList = $this->getForm("dialogAccessList"); + $this->view->formDialogBasicAuth = $this->getForm("dialogBasicAuth"); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml new file mode 100644 index 000000000..b38a67826 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml @@ -0,0 +1,28 @@ +
+ + accesslist.accesslistName + + text + + + + accesslist.clientIps + + select_multiple + + true + + + + accesslist.accesslistInvert + + checkbox + + + + accesslist.description + + text + + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml new file mode 100644 index 000000000..18482bb3f --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml @@ -0,0 +1,20 @@ +
+ + basicauth.basicauthuser + + text + + + + basicauth.basicauthpass + + text + + + + basicauth.description + + text + + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml new file mode 100644 index 000000000..26bb9cacb --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml @@ -0,0 +1,91 @@ +
+ + handle.enabled + + checkbox + + + + handle.reverse + + dropdown + + + + handle.subdomain + + dropdown + + true + + + handle.HandleType + + dropdown + + true + + + handle.HandlePath + + text + + true + + + handle.ToDomain + + text + 192.168.1.1 + + + + handle.ToPort + + text + 443 + + true + + + handle.ToPath + + text + + true + + + handle.HttpTls + + checkbox + + true + + + handle.HttpTlsTrustedCaCerts + + dropdown + + true + + + handle.HttpTlsServerName + + text + + true + + + handle.HttpNtlm + + checkbox + + true + + + handle.description + + text + + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml new file mode 100644 index 000000000..16699cf3a --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml @@ -0,0 +1,71 @@ +
+ + reverse.enabled + + checkbox + + + + reverse.FromDomain + + text + example.com + + + + reverse.FromPort + + text + 443 + + true + + + reverse.accesslist + + dropdown + + true + + + reverse.basicauth + + select_multiple + 5 + + true + + + reverse.DnsChallenge + + checkbox + + + + reverse.DynDns + + checkbox + + + + reverse.CustomCertificate + + dropdown + + true + + + reverse.AccessLog + + checkbox + + true + + + reverse.description + + text + example.com.443 + + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml new file mode 100644 index 000000000..a43e961ee --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml @@ -0,0 +1,57 @@ +
+ + subdomain.enabled + + checkbox + + + + subdomain.reverse + + dropdown + + + + subdomain.FromDomain + + text + opn.example.com + + + + subdomain.FromPort + + text + 443 + + true + + + subdomain.accesslist + + dropdown + + true + + + subdomain.basicauth + + select_multiple + 5 + + true + + + subdomain.DynDns + + checkbox + + + + subdomain.description + + text + opn.example.com.443 + + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dnsprovider.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dnsprovider.xml new file mode 100644 index 000000000..ad963b984 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dnsprovider.xml @@ -0,0 +1,63 @@ +
+ + caddy.general.TlsDnsProvider + + dropdown + + + + caddy.general.TlsDnsApiKey + + text + + + + caddy.general.TlsDnsSecretApiKey + + text + + true + + + caddy.general.TlsDnsOptionalField1 + + text + + true + + + caddy.general.TlsDnsOptionalField2 + + text + + true + + + caddy.general.TlsDnsOptionalField3 + + text + + true + + + caddy.general.TlsDnsOptionalField4 + + text + + true + + + caddy.general.TlsDnsOptionalField5 + + text + + true + + + caddy.general.TlsDnsOptionalField6 + + text + + true + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dynamicdns.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dynamicdns.xml new file mode 100644 index 000000000..836154910 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dynamicdns.xml @@ -0,0 +1,34 @@ +
+ + caddy.general.DynDnsSimpleHttp + + text + + true + + + caddy.general.DynDnsInterface + + dropdown + + true + + + caddy.general.DynDnsCheckInterval + + text + + + + caddy.general.DynDnsIpVersions + + dropdown + + + + caddy.general.DynDnsTTL + + text + + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml new file mode 100644 index 000000000..b00d7085e --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml @@ -0,0 +1,33 @@ +
+ + caddy.general.enabled + + checkbox + + + + caddy.general.TlsEmail + + text + info@example.com + + + + caddy.general.TlsAutoHttps + + dropdown + + + + caddy.general.abort + + checkbox + + + + caddy.general.accesslist + + dropdown + + +
diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/logsettings.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/logsettings.xml new file mode 100644 index 000000000..5af8a9a14 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/logsettings.xml @@ -0,0 +1,23 @@ +
+ + caddy.general.LogCredentials + + checkbox + + + + caddy.general.LogAccessPlain + + checkbox + + true + + + caddy.general.LogAccessPlainKeep + + 10 + text + + true + +
diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/ACL/ACL.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/ACL/ACL.xml new file mode 100644 index 000000000..c98f84a7e --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/ACL/ACL.xml @@ -0,0 +1,18 @@ + + + Services: Caddy Web Server: General Settings + Allow access to Caddy General Settings + + ui/caddy/general/* + api/caddy/general/* + + + + Services: Caddy Web Server: Reverse Proxy + Allow access to Caddy Reverse Proxy + + ui/caddy/reverse_proxy/* + api/caddy/reverse_proxy/* + + + diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php new file mode 100644 index 000000000..e9bbb97df --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php @@ -0,0 +1,160 @@ +__reference; // Dynamic key based on item reference + $fromDomainOrSubdomain = (string) $item->FromDomain; + $fromPort = (string) $item->FromPort; + + if ($fromPort === '') { + $defaultPorts = ['80', '443']; + } else { + $defaultPorts = [$fromPort]; + } + + foreach ($defaultPorts as $port) { + // Create a unique key for domain/subdomain-port combination + $comboKey = $fromDomainOrSubdomain . ':' . $port; + + // Check for duplicate combinations + if (isset($combos[$comboKey])) { + // Use dynamic $key for message referencing + $messages->appendMessage(new Message( + "Duplicate entry: The combination of $type '$fromDomainOrSubdomain' and port '$port' is already used. Each $type and port pairing must be unique.", + $type === 'domain' ? $key . ".FromDomain" : $key . ".FromDomain", // Adjusted to use dynamic key + "Duplicate" . ucfirst($type) . "Port" + )); + } else { + $combos[$comboKey] = true; + } + } + } + } + + // 3. Check that subdomains are under a wildcard or exact domain + private function checkSubdomainsAgainstDomains($subdomains, $domains, $messages) + { + $wildcardDomainList = []; + foreach ($domains as $domain) { + if ((string) $domain->enabled === '1') { + $domainName = (string) $domain->FromDomain; + if (str_starts_with($domainName, '*.')) { + $wildcardBase = substr($domainName, 2); + $wildcardDomainList[$wildcardBase] = $domainName; + } + } + } + + foreach ($subdomains as $subdomain) { + if ((string) $subdomain->enabled === '1') { + $subdomainName = (string) $subdomain->FromDomain; + $isValid = false; + foreach ($wildcardDomainList as $baseDomain => $wildcardDomain) { + if (str_ends_with($subdomainName, $baseDomain)) { + $isValid = true; + break; + } + } + + if (!$isValid) { + $key = $subdomain->__reference; // Dynamic key based on subdomain reference + $messages->appendMessage(new Message( + "Invalid subdomain configuration: '$subdomainName' does not fall under any configured wildcard domain.", + $key . ".FromDomain", // Use dynamic key for message referencing + "InvalidSubdomain" + )); + } + } + } + } + + // 4. Check for conflicts between wildcard and base domains + private function checkForWildcardAndBaseDomainConflicts($domains, $messages) + { + $domainList = []; + foreach ($domains as $domain) { + if ((string) $domain->enabled === '1') { + $domainName = (string) $domain->FromDomain; + $domainList[$domainName] = true; + + // Check for wildcard or base domain conflict + if (str_starts_with($domainName, '*.')) { + $baseDomain = substr($domainName, 2); + if (isset($domainList[$baseDomain])) { + $key = $domain->__reference; // Dynamic key based on domain reference + $messages->appendMessage(new Message( + "Invalid domain configuration: Cannot create wildcard domain '$domainName' because base domain '$baseDomain' exists.", + $key . ".FromDomain", // Use dynamic key for message referencing + "WildcardBaseConflict" + )); + } + } else { + $wildcardDomain = '*.' . $domainName; + if (isset($domainList[$wildcardDomain])) { + $key = $domain->__reference; // Dynamic key based on domain reference + $messages->appendMessage(new Message( + "Invalid domain configuration: Cannot create base domain '$domainName' because wildcard domain '$wildcardDomain' exists.", + $key . ".FromDomain", // Use dynamic key for message referencing + "BaseWildcardConflict" + )); + } + } + } + } + } + + // Perform the actual validation + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + // 1. Check domain-port combinations + $this->checkForUniquePortCombos($this->reverseproxy->reverse->iterateItems(), $messages, 'domain'); + // 2. Check subdomain-port combinations + $this->checkForUniquePortCombos($this->reverseproxy->subdomain->iterateItems(), $messages, 'subdomain'); + // 3. Check that subdomains are under a wildcard or exact domain + $this->checkSubdomainsAgainstDomains($this->reverseproxy->subdomain->iterateItems(), $this->reverseproxy->reverse->iterateItems(), $messages); + // 4. Check for conflicts between wildcard and base domains + $this->checkForWildcardAndBaseDomainConflicts($this->reverseproxy->reverse->iterateItems(), $messages); + + return $messages; + } +} diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml new file mode 100644 index 000000000..0c95d2b7d --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml @@ -0,0 +1,330 @@ + + //Pischem/caddy + A GUI model for configuring a reverse proxy in the Caddy web server. + 1.1.3 + + + + 0 + Y + + + Please enter a valid email address. + + + on + + On (default) + Off + Disable Redirects + Disable Certs + Ignore Loaded Certs + + + + + None (default) + Cloudflare + Duck DNS + DigitalOcean + DNSPod + Hetzner + GoDaddy + Gandi + IONOS + Desec + Porkbun + Route53 + ACME-DNS + Alidns + Google Cloud DNS + Azure + OpenStack Designate + OVH + Namecheap + Netlify + Namesilo + PowerDNS + Vercel + DDNSS + Njalla + Metaname + Linode + Tencent Cloud + Dinahosting + Hexonet + Mail-in-a-Box + + + + + + + + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.accesslist + accesslistName + + + + + 0 + + + 0 + + + 0 + + + 10 + 1 + Please enter a valid number of 1 or larger. + + + Please enter a valid URL, starting with http or https. + + + + 5 + 1 + 1440 + Please enter a valid number from 1 to 1440 minutes. + + + ipv4 + + IPv4 only + IPv6 only + + + + 1 + 1 + 24 + Please enter a valid number from 1 to 24 hours. + + + + + + 1 + Y + + + Y + Please enter a valid 'from' domain or IP address. + Y + Y + Y + N + + + Please enter a valid 'from' port number. + Y + N + + + + + OPNsense.Caddy.Caddy + reverseproxy.accesslist + accesslistName + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.basicauth + basicauthuser + + + Y + + + Y + /^([\t\n\v\f\r 0-9a-zA-Z.,_*-\x{00A0}-\x{FFFF}]){1,255}$/u + Please provide a valid description. + + + 0 + + + + 0 + + + 0 + + + + + 1 + Y + + + Y + + + OPNsense.Caddy.Caddy + reverseproxy.reverse + description + + + + + Y + Please enter a valid 'from' Subdomain that is based upon the wildcard domain. + N + + + Please enter a valid 'from' port number. + Y + N + + + + + OPNsense.Caddy.Caddy + reverseproxy.accesslist + accesslistName + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.basicauth + basicauthuser + + + Y + + + Y + /^([\t\n\v\f\r 0-9a-zA-Z.,_-\x{00A0}-\x{FFFF}]){1,255}$/u + Please provide a valid description. + + + 0 + + + + + 1 + Y + + + Y + + + OPNsense.Caddy.Caddy + reverseproxy.reverse + description + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.subdomain + description + + + + + Y + handle + + handle + handle_path + + + + /^(\/.*)?$/u + Please enter a valid 'Handle Path' that starts with '/'. + + + Y + Please enter a valid 'to' domain or IP address. + Y + + + Please enter a valid 'to' port number. + Y + N + + + /^(\/.*)?$/u + Please enter a valid 'Backend Path' that starts with '/'. + + + 0 + + + 0 + + + ca + + + Please enter a valid hostname or IP address. + Y + Y + Y + N + + + /^([\t\n\v\f\r 0-9a-zA-Z.,_-\x{00A0}-\x{FFFF}]){1,255}$/u + Please provide a valid description. + + + + + Y + /^([\t\n\v\f\r 0-9a-zA-Z.,_*-\x{00A0}-\x{FFFF}]){1,255}$/u + Please provide a valid Access List Name. + + + Y + Y + , + Y + Y + Please enter valid IP address(es) or network(s), separated by commas. + + + 0 + + + /^([\t\n\v\f\r 0-9a-zA-Z.,_*-\x{00A0}-\x{FFFF}]){1,255}$/u + Please provide a valid description. + + + + + Y + /^([0-9a-zA-Z]{2,72})$/u + A user name must only contain numbers and letters and must be between 2 and 72 characters. + + + Y + + + /^([\t\n\v\f\r 0-9a-zA-Z.,_-\x{00A0}-\x{FFFF}]){1,255}$/u + Please provide a valid description. + + + + + diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Menu/Menu.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Menu/Menu.xml new file mode 100644 index 000000000..60886d550 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Menu/Menu.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Migrations/M1_1_3.php b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Migrations/M1_1_3.php new file mode 100644 index 000000000..a08a403b1 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Migrations/M1_1_3.php @@ -0,0 +1,80 @@ +object(); + + // Ensure there are reverse proxy configurations to process + if (!empty($config->Pischem->caddy->reverseproxy)) { + + // Loop through each reverse proxy configuration in the stored configuration config.xml + foreach ($config->Pischem->caddy->reverseproxy->children() as $configNode) { + + // Extract the UUID attribute to identify the configuration item + $uuid = (string)$configNode->attributes()->uuid; + + // Check if the current configuration item has a 'Description' to migrate + if (!empty($configNode->Description)) { + + // Store the value of 'Description' for migration + $descriptionValue = (string)$configNode->Description; + + // Attempt to locate the corresponding node in the model using the UUID + $modelNode = null; + + // Retrieve reverse proxy items from the model for matching UUID + $reverseProxies = $model->getNodeByReference('reverseproxy')->iterateItems(); + foreach ($reverseProxies as $item) { + foreach ($item->iterateItems() as $modelUuid => $node) { + if ($uuid === $modelUuid) { + $modelNode = $node; + break 2; // Break from both loops once the node is found + } + } + } + + // If a matching node is found in the model, migrate the 'Description' value to 'description' value + if ($modelNode !== null) { + $modelNode->description = $descriptionValue; + } + } + } + } + + // Model is saved by 'run_migrations.php' + } +} diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt new file mode 100644 index 000000000..0b07581a4 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt @@ -0,0 +1,194 @@ +{# + # Copyright (c) 2023-2024 Cedrik Pischem + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without modification, + # are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright notice, + # this list of conditions and the following disclaimer in the documentation + # and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + + + + + +
+ +
+ {{ partial("layout_partials/base_form", ['fields': generalForm, 'action': '/ui/caddy/general', 'id': 'frm_GeneralSettings']) }} +
+ +
+ {{ partial("layout_partials/base_form", ['fields': dnsproviderForm, 'action': '/ui/caddy/general', 'id': 'frm_GeneralSettings']) }} +
+ +
+ {{ partial("layout_partials/base_form", ['fields': dynamicdnsForm, 'action': '/ui/caddy/general', 'id': 'frm_GeneralSettings']) }} +
+ +
+ {{ partial("layout_partials/base_form", ['fields': logsettingsForm, 'action': '/ui/caddy/general', 'id': 'frm_GeneralSettings']) }} +
+
+ +
+
+
+
+ + + +

+ + +
+
+
diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt new file mode 100644 index 000000000..4c9750ee2 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt @@ -0,0 +1,351 @@ +{# + # Copyright (c) 2023-2024 Cedrik Pischem + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without modification, + # are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright notice, + # this list of conditions and the following disclaimer in the documentation + # and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + + + +
+ + +
+
+ +

Domains

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDEnabledDomainPortAccess ListBasic AuthDNS-01Dynamic DNSHTTP Access LogCustom CertificateDescriptionCommands
+ + +
+
+
+
+ +

Subdomains

+
+ + + + + + + + + + + + + + + + + + + + + + + +
IDEnabledDomainSubdomainPortAccess ListBasic AuthDynamic DNSDescriptionCommands
+ + +
+
+
+
+ + +
+
+

Handlers

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDEnabledDomainSubdomainHandle TypeHandle PathBackend DomainBackend PortBackend PathTLSTLS CATLS Server NameNTLMDescriptionCommands
+ + +
+
+
+
+ + +
+ +
+

Access Lists

+
+ + + + + + + + + + + + + + + + + + + +
IDNameClient IPsInvertDescriptionCommands
+ + +
+
+
+ + +
+

Basic Auth

+
+ + + + + + + + + + + + + + + + + +
IDUserDescriptionCommands
+ + +
+
+
+
+
+ + +
+
+
+
+ +

+ + + + +
+
+
+ +{{ partial("layout_partials/base_dialog",['fields':formDialogReverseProxy,'id':'DialogReverseProxy','label':lang._('Edit Reverse Proxy Domain')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogSubdomain,'id':'DialogSubdomain','label':lang._('Edit Reverse Proxy Subdomain')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogHandle,'id':'DialogHandle','label':lang._('Edit Handler')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogAccessList,'id':'DialogAccessList','label':lang._('Edit Access List')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogBasicAuth,'id':'DialogBasicAuth','label':lang._('Edit Basic Auth')])}} diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php new file mode 100755 index 000000000..df8550d33 --- /dev/null +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php @@ -0,0 +1,86 @@ +#!/usr/local/bin/php +object(); +$temp_dir = '/usr/local/etc/caddy/certificates/temp/'; + +function extract_and_save_certificates($configObj, $temp_dir) { + // Traverse through certificates + foreach ($configObj->cert as $cert) { + $cert_refid = (string)$cert->refid; + $cert_content = base64_decode((string)$cert->crt); + $key_content = base64_decode((string)$cert->prv); + $cert_chain = $cert_content; + + // Handle CA and possible intermediate CA to create a certificate bundle + if (!empty($cert->caref)) { + foreach ($configObj->ca as $ca) { + if ((string)$cert->caref == (string)$ca->refid) { + $ca_content = base64_decode((string)$ca->crt); + $cert_chain .= "\n" . $ca_content; + + if (!empty($ca->caref)) { + foreach ($configObj->ca as $parent_ca) { + if ((string)$ca->caref == (string)$parent_ca->refid) { + $parent_ca_content = base64_decode((string)$parent_ca->crt); + $cert_chain .= "\n" . $parent_ca_content; + break; + } + } + } + } + } + } + + // Save the certificate chain and private key + file_put_contents($temp_dir . $cert_refid . '.pem', $cert_chain); + chmod($temp_dir . $cert_refid . '.pem', 0600); + file_put_contents($temp_dir . $cert_refid . '.key', $key_content); + chmod($temp_dir . $cert_refid . '.key', 0600); + } + + // Traverse through CA certificates and save them + foreach ($configObj->ca as $ca) { + $ca_refid = (string)$ca->refid; + $ca_content = base64_decode((string)$ca->crt); + + // Save the CA certificate + file_put_contents($temp_dir . $ca_refid . '.pem', $ca_content); + chmod($temp_dir . $ca_refid . '.pem', 0600); + } +} + +extract_and_save_certificates($configObj, $temp_dir); diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py new file mode 100755 index 000000000..2fc795ab4 --- /dev/null +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py @@ -0,0 +1,80 @@ +#!/usr/local/bin/python3 + +# +# Copyright (c) 2023-2024 Cedrik Pischem +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +import subprocess +import json +import sys + +def run_service_command(action, action_message): + result = {"message": action_message} + + if action == "validate": + try: + # Call Setup script + subprocess.run(["/usr/local/opnsense/scripts/OPNsense/Caddy/setup.sh"], check=True) + # Validate the Caddyfile with explicit --config flag, capturing both stdout and stderr + validation_output = subprocess.check_output(["caddy", "validate", "--config", "/usr/local/etc/caddy/Caddyfile"], stderr=subprocess.STDOUT, text=True) + if "Valid configuration" in validation_output: + result["status"] = "ok" + result["message"] = "Caddy configuration is valid." + else: + # Search for the specific error message + error_msg = next((line for line in validation_output.split('\n') if line.startswith("Error:")), "Caddy configuration is not valid.") + result["status"] = "failed" + result["message"] = error_msg + except subprocess.CalledProcessError as e: + # Extracting only the specific "Error: ..." line from the output + error_msg = next((line for line in e.output.split('\n') if line.startswith("Error:")), "Validation failed.") + result["status"] = "failed" + result["message"] = error_msg + else: + try: + subprocess.run(["service", "caddy", action], check=True) + result["status"] = "ok" + except subprocess.CalledProcessError as e: + result["status"] = "failed" + result["message"] = str(e) + + return json.dumps(result) + +# Updated actions dictionary +actions = { + "start": "onestart", + "stop": "onestop", + "restart": "onerestart", + "validate": "validate" # Validate action +} + +if __name__ == "__main__": + action = sys.argv[1] # Get the action from the command-line argument + if action in actions: + service_action = actions[action] + message = f"{action.capitalize()}ing Caddy service" if action != "validate" else "Validating Caddy configuration" + print(run_service_command(service_action, message)) + else: + print(json.dumps({"status": "failed", "message": f"Unknown action: {action}"})) diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh new file mode 100755 index 000000000..6ee35ebb7 --- /dev/null +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Define directories +CADDY_DIR="/usr/local/etc/caddy" +CADDY_ACME_DIR="${CADDY_DIR}/acme" +CADDY_CERTS_DIR="${CADDY_DIR}/certificates/temp" +CADDY_OCSP_DIR="${CADDY_DIR}/ocsp" +CADDY_LOCKS_DIR="${CADDY_DIR}/locks" +CADDY_LOG_DIR="/var/log/caddy/access" +CADDY_CONF_DIR="${CADDY_DIR}/caddy.d" + +# Create Caddy configuration directories with appropriate permissions +mkdir -p "${CADDY_DIR}" +mkdir -p "${CADDY_ACME_DIR}" +mkdir -p "${CADDY_CERTS_DIR}" +mkdir -p "${CADDY_OCSP_DIR}" +mkdir -p "${CADDY_LOCKS_DIR}" +mkdir -p "${CADDY_CONF_DIR}" + +# Set permissions for Caddy configuration directories +chown -R root:wheel "${CADDY_DIR}" +chmod -R 750 "${CADDY_DIR}" + +# Create Caddy log directory +mkdir -p "${CADDY_LOG_DIR}" + +# Set permissions for Caddy log directory +chown -R root:wheel "${CADDY_LOG_DIR}" +chmod -R 750 "${CADDY_LOG_DIR}" + +# Format and overwrite the Caddyfile +(cd "${CADDY_DIR}" && /usr/local/bin/caddy fmt --overwrite) + +# Write custom certs from the OPNsense Trust Store into a directory where Caddy can read them +/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_certs.php + +# Optional Debug message +# echo "Caddy installation completed. All caddy directories and files created successfully." diff --git a/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf new file mode 100644 index 000000000..764809f8e --- /dev/null +++ b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf @@ -0,0 +1,29 @@ +[start] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py start +parameters: +type:script +message:Starting Caddy service + +[stop] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py stop +parameters: +type:script +message:Stopping Caddy service + +[restart] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py restart +parameters: +type:script +message:Reloading Caddy configuration + +[validate] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py validate +parameters: +type:script_output +message:Validating Caddy configuration + +[status] +command:/usr/local/sbin/pluginctl -s caddy status +parameters: +type:script_output +message:Request Caddy status diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/+TARGETS b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/+TARGETS new file mode 100644 index 000000000..194242c0a --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/+TARGETS @@ -0,0 +1,2 @@ +Caddyfile:/usr/local/etc/caddy/Caddyfile +rc.conf.d/caddy:/etc/rc.conf.d/caddy diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile new file mode 100644 index 000000000..95892a590 --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile @@ -0,0 +1,573 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file + +{% set generalSettings = helpers.getNodeByTag('Pischem.caddy.general') %} + +# Global Options +{ + storage file_system { + root /usr/local/etc/caddy + } + log { + {% if generalSettings.LogAccessPlain|default("0") == "0" %} + {% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} + {% if reverse.enabled|default("0") == "1" and reverse.AccessLog|default("0") == "1" %} + include http.log.access.{{ reverse['@uuid'] }} + {% endif %} + {% endfor %} + {% endif %} + output net unixgram//var/caddy/var/run/log { + } + format json { + time_format rfc3339 + } + } + + {% set accessListUuid = generalSettings.accesslist %} + {% set logCredentials = generalSettings.LogCredentials %} + + {% set hasAccessList = false %} + {% set hasLogCredentials = false %} + + {% if accessListUuid %} + {% set accessList = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', accessListUuid) | first %} + {% if accessList %} + {% set hasAccessList = true %} + {% endif %} + {% endif %} + + {% if logCredentials == '1' %} + {% set hasLogCredentials = true %} + {% endif %} + + {% if hasAccessList or hasLogCredentials %} + servers { + {% if hasAccessList %} + trusted_proxies static {{ accessList.clientIps.split(',') | join(' ') }} + {% endif %} + {% if hasLogCredentials %} + log_credentials + {% endif %} + } + {% endif %} + + {% set dnsProvider = helpers.toList('Pischem.caddy.general.TlsDnsProvider') | first %} + {% set dnsApiKey = generalSettings.TlsDnsApiKey %} + {% set dnsSecretApiKey = generalSettings.TlsDnsSecretApiKey %} + {% set dnsOptionalField1 = generalSettings.TlsDnsOptionalField1 %} + {% set dnsOptionalField2 = generalSettings.TlsDnsOptionalField2 %} + {% set dnsOptionalField3 = generalSettings.TlsDnsOptionalField3 %} + {% set dnsOptionalField4 = generalSettings.TlsDnsOptionalField4 %} + {% set dnsOptionalField5 = generalSettings.TlsDnsOptionalField5 %} + {% set dnsOptionalField6 = generalSettings.TlsDnsOptionalField6 %} + {% set dynDnsSimpleHttp = generalSettings.DynDnsSimpleHttp %} + {% set dynDnsInterface = generalSettings.DynDnsInterface %} + {% set dynDnsCheckInterval = generalSettings.DynDnsCheckInterval %} + {% set dynDnsIpVersions = generalSettings.DynDnsIpVersions %} + {% set dynDnsTTL = generalSettings.DynDnsTTL %} + {% set dynDnsDomains = [] %} + + {% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} + {% if reverse.enabled|default("0") == "1" and reverse.DynDns|default("0") == "1" %} + {% set cleanedDomain = reverse.FromDomain | replace("*.","") %} + {% do dynDnsDomains.append(cleanedDomain + " @") %} + {% endif %} + + {% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %} + {% if subdomain.enabled|default("0") == "1" and subdomain.DynDns|default("0") == "1" and subdomain.reverse == reverse['@uuid'] %} + {% set fullSubdomain = subdomain.FromDomain %} + {% set baseDomain = fullSubdomain.split('.')[1:] | join('.') %} + {% set subDomainPart = fullSubdomain.split('.')[0] %} + {% set subdomainEntry = baseDomain + " " + subDomainPart %} + {% do dynDnsDomains.append(subdomainEntry) %} + {% endif %} + {% endfor %} + {% endfor %} + + {% if dnsProvider and dnsProvider != "none" and dnsProvider != "acmedns" and dynDnsDomains|length > 0 %} + dynamic_dns { + {% if dnsProvider in ['porkbun', 'desec', 'route53', 'alidns', 'googleclouddns', 'azure', 'openstack-designate', 'ovh', 'namecheap', 'powerdns', 'ddnss', 'linode', 'tencentcloud', 'dinahosting', 'hexonet', 'mailinabox'] %} + provider {{ dnsProvider }} { + {% if dnsProvider == 'porkbun' %} + {% if dnsApiKey %}api_key {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}api_secret_key {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'desec' %} + {% if dnsApiKey %}token {{ dnsApiKey }} + {% endif %} + {% elif dnsProvider == 'route53' %} + {% if dnsApiKey %}access_key_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}secret_access_key {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}max_retries {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}aws_profile {{ dnsOptionalField2 }} + {% endif %} + {% if dnsOptionalField3 %}region {{ dnsOptionalField3 }} + {% endif %} + {% if dnsOptionalField4 %}token {{ dnsOptionalField4 }} + {% endif %} + {% elif dnsProvider == 'alidns' %} + {% if dnsApiKey %}access_key_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}access_key_secret {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'googleclouddns' %} + {% if dnsApiKey %}gcp_project {{ dnsApiKey }} + {% endif %} + {% elif dnsProvider == 'azure' %} + {% if dnsApiKey %}tenant_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}client_id {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}client_secret {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}subscription_id {{ dnsOptionalField2 }} + {% endif %} + {% if dnsOptionalField3 %}resource_group_name {{ dnsOptionalField3 }} + {% endif %} + {% elif dnsProvider == 'openstack-designate' %} + {% if dnsApiKey %}region_name {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}tenant_id {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}identity_api_version {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}password {{ dnsOptionalField2 }} + {% endif %} + {% if dnsOptionalField3 %}username {{ dnsOptionalField3 }} + {% endif %} + {% if dnsOptionalField4 %}tenant_name {{ dnsOptionalField4 }} + {% endif %} + {% if dnsOptionalField5 %}auth_url {{ dnsOptionalField5 }} + {% endif %} + {% if dnsOptionalField6 %}endpoint_type {{ dnsOptionalField6 }} + {% endif %} + {% elif dnsProvider == 'ovh' %} + {% if dnsApiKey %}endpoint {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}application_key {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}application_secret {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}consumer_key {{ dnsOptionalField2 }} + {% endif %} + {% elif dnsProvider == 'namecheap' %} + {% if dnsApiKey %}api_key {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}user {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}api_endpoint {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}client_ip {{ dnsOptionalField2 }} + {% endif %} + {% elif dnsProvider == 'powerdns' %} + {% if dnsApiKey %}server_url {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}api_token {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'ddnss' %} + {% if dnsApiKey %}api_token {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}username {{ dnsSecretApiKey }} + {% endif %} + password {{ dnsOptionalField1 }} + {% elif dnsProvider == 'linode' %} + {% if dnsApiKey %}api_token {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}api_url {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}api_version {{ dnsOptionalField1 }} + {% endif %} + {% elif dnsProvider == 'tencentcloud' %} + {% if dnsApiKey %}secret_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}secret_key {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'dinahosting' %} + {% if dnsApiKey %}username {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'hexonet' %} + {% if dnsApiKey %}username {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'mailinabox' %} + {% if dnsApiKey %}api_url {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}email_address {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}password {{ dnsOptionalField1 }} + {% endif %} + {% endif %} + } + {% elif dnsProvider in ['metaname'] %} + provider {{ dnsProvider }} {{ dnsApiKey }} {{ dnsSecretApiKey }} + {% else %} + provider {{ dnsProvider }} {{ dnsApiKey }} + {% endif %} + domains { + {% for domain in dynDnsDomains %} + {{ domain }} + {% endfor %} + } + {% if dynDnsSimpleHttp %} + ip_source simple_http {{ dynDnsSimpleHttp }} + {% endif %} + {% if dynDnsInterface %} + {% set physicalInterfaceNames = [] %} + {% for intfName in dynDnsInterface.split(',') %} + {% do physicalInterfaceNames.append(helpers.physical_interface(intfName)) %} + {% endfor %} + ip_source interface {{ physicalInterfaceNames | join(',') }} + {% endif %} + {% if dynDnsCheckInterval %} + check_interval {{ dynDnsCheckInterval }}m + {% endif %} + {% if dynDnsIpVersions %} + versions {{ dynDnsIpVersions }} + {% endif %} + {% if dynDnsTTL %} + ttl {{ dynDnsTTL }}h + {% endif %} + } + {% endif %} + + {% set emailValue = helpers.toList('Pischem.caddy.general.TlsEmail') | first %} + {% if emailValue %} + email {{ emailValue }} + {% endif %} + {% set autoHttpsValue = helpers.toList('Pischem.caddy.general.TlsAutoHttps') | first %} + {% if autoHttpsValue != "on" %} + auto_https {{ autoHttpsValue }} + {% endif %} + import /usr/local/etc/caddy/caddy.d/*.global +} + +# Reverse Proxy Configuration +{% macro tls_configuration(dnsProvider, dnsApiKey, customCert, dnsChallenge, dnsSecretApiKey, TlsDnsOptionalField1, TlsDnsOptionalField2, TlsDnsOptionalField3, TlsDnsOptionalField4, TlsDnsOptionalField5, TlsDnsOptionalField6) %} + {% if dnsChallenge == "1" and dnsProvider and dnsProvider != "none" %} + {% if dnsProvider in ['duckdns', 'porkbun', 'desec', 'route53', 'acmedns', 'alidns', 'googleclouddns', 'azure', 'openstack-designate', 'ovh', 'namecheap', 'powerdns', 'ddnss', 'linode', 'tencentcloud', 'dinahosting', 'hexonet', 'mailinabox'] %} + tls { + dns {{ dnsProvider }} { + {% if dnsProvider == 'duckdns' %} + {% if dnsApiKey %}api_token {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}override_domain {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'porkbun' %} + {% if dnsApiKey %}api_key {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}api_secret_key {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'desec' %} + {% if dnsApiKey %}token {{ dnsApiKey }} + {% endif %} + {% elif dnsProvider == 'route53' %} + {% if dnsApiKey %}access_key_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}secret_access_key {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}max_retries {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}aws_profile {{ dnsOptionalField2 }} + {% endif %} + {% if dnsOptionalField3 %}region {{ dnsOptionalField3 }} + {% endif %} + {% if dnsOptionalField4 %}token {{ dnsOptionalField4 }} + {% endif %} + {% elif dnsProvider == 'acmedns' %} + {% if dnsApiKey %}username {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}subdomain {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}server_url {{ dnsOptionalField2 }} + {% endif %} + {% elif dnsProvider == 'alidns' %} + {% if dnsApiKey %}access_key_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}access_key_secret {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'googleclouddns' %} + {% if dnsApiKey %}gcp_project {{ dnsApiKey }} + {% endif %} + {% elif dnsProvider == 'azure' %} + {% if dnsApiKey %}tenant_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}client_id {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}client_secret {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}subscription_id {{ dnsOptionalField2 }} + {% endif %} + {% if dnsOptionalField3 %}resource_group_name {{ dnsOptionalField3 }} + {% endif %} + {% elif dnsProvider == 'openstack-designate' %} + {% if dnsApiKey %}region_name {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}tenant_id {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}identity_api_version {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}password {{ dnsOptionalField2 }} + {% endif %} + {% if dnsOptionalField3 %}username {{ dnsOptionalField3 }} + {% endif %} + {% if dnsOptionalField4 %}tenant_name {{ dnsOptionalField4 }} + {% endif %} + {% if dnsOptionalField5 %}auth_url {{ dnsOptionalField5 }} + {% endif %} + {% if dnsOptionalField6 %}endpoint_type {{ dnsOptionalField6 }} + {% endif %} + {% elif dnsProvider == 'ovh' %} + {% if dnsApiKey %}endpoint {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}application_key {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}application_secret {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}consumer_key {{ dnsOptionalField2 }} + {% endif %} + {% elif dnsProvider == 'namecheap' %} + {% if dnsApiKey %}api_key {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}user {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}api_endpoint {{ dnsOptionalField1 }} + {% endif %} + {% if dnsOptionalField2 %}client_ip {{ dnsOptionalField2 }} + {% endif %} + {% elif dnsProvider == 'powerdns' %} + {% if dnsApiKey %}server_url {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}api_token {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'ddnss' %} + {% if dnsApiKey %}api_token {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}username {{ dnsSecretApiKey }} + {% endif %} + password {{ dnsOptionalField1 }} + {% elif dnsProvider == 'linode' %} + {% if dnsApiKey %}api_token {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}api_url {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}api_version {{ dnsOptionalField1 }} + {% endif %} + {% elif dnsProvider == 'tencentcloud' %} + {% if dnsApiKey %}secret_id {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}secret_key {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'dinahosting' %} + {% if dnsApiKey %}username {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'hexonet' %} + {% if dnsApiKey %}username {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}password {{ dnsSecretApiKey }} + {% endif %} + {% elif dnsProvider == 'mailinabox' %} + {% if dnsApiKey %}api_url {{ dnsApiKey }} + {% endif %} + {% if dnsSecretApiKey %}email_address {{ dnsSecretApiKey }} + {% endif %} + {% if dnsOptionalField1 %}password {{ dnsOptionalField1 }} + {% endif %} + {% endif %} + } + } + {% elif dnsProvider in ['metaname'] %} + tls { + dns {{ dnsProvider }} {{ dnsApiKey }} {{ dnsSecretApiKey }} + } + {% else %} + tls { + dns {{ dnsProvider }} {{ dnsApiKey }} + } + {% endif %} + {% endif %} + {% if customCert %} + tls /usr/local/etc/caddy/certificates/temp/{{ customCert }}.pem /usr/local/etc/caddy/certificates/temp/{{ customCert }}.key + {% endif %} +{% endmacro %} + +{% macro reverse_proxy_configuration(handle) %} + {{ handle.HandleType }} {{ handle.HandlePath|default("") }} { + {% if handle.ToPath|default("") != "" %} + rewrite * {{ handle.ToPath }}{uri} + {% endif %} + reverse_proxy {{ handle.ToDomain }}{% if handle.ToPort %}:{{ handle.ToPort }}{% endif %} { + {% if handle.HttpTls|default("0") == "1" %} + {% if handle.HttpNtlm|default("0") == "1" %} + transport http_ntlm { + tls + {% if handle.HttpTlsTrustedCaCerts %} + tls_trusted_ca_certs /usr/local/etc/caddy/certificates/temp/{{ handle.HttpTlsTrustedCaCerts }}.pem + {% endif %} + {% if handle.HttpTlsServerName %} + tls_server_name {{ handle.HttpTlsServerName }} + {% endif %} + } + {% else %} + transport http { + tls + {% if handle.HttpTlsTrustedCaCerts %} + tls_trusted_ca_certs /usr/local/etc/caddy/certificates/temp/{{ handle.HttpTlsTrustedCaCerts }}.pem + {% endif %} + {% if handle.HttpTlsServerName %} + tls_server_name {{ handle.HttpTlsServerName }} + {% endif %} + } + {% endif %} + {% endif %} + } + } +{% endmacro %} + +{% macro access_list_configuration(accesslist, invert) %} + {% set client_ips = accesslist.clientIps.split(',') %} + {% set client_ips_space_separated = client_ips | join(' ') %} + @{{ accesslist['@uuid'] }} { + {{ 'not' if invert else '' }} client_ip {{ client_ips_space_separated }} + } +{% endmacro %} + +{% macro basicauth_configuration(basicauth_uuids) %} + {% if basicauth_uuids %} + basicauth { + {% for uuid in basicauth_uuids.split(',') %} + {% set basicauth = helpers.toList('Pischem.caddy.reverseproxy.basicauth') | selectattr('@uuid', 'equalto', uuid) | first %} + {% if basicauth %} + {{ basicauth.basicauthuser }} {{ basicauth.basicauthpass }} + {% endif %} + {% endfor %} + } + {% endif %} +{% endmacro %} + +{% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} +{% if reverse.enabled|default("0") == "1" %} +# Reverse Proxy Domain: "{{ reverse['@uuid'] }}" +{{ reverse.FromDomain|default("") }}{% if reverse.FromPort %}:{{ reverse.FromPort }}{% endif %} { + {% if reverse.AccessLog|default("0") == "1" %} + {% if generalSettings.LogAccessPlain|default("0") == "0" %} + log {{ reverse['@uuid'] }} + {% else %} + log { + output file /var/log/caddy/access/{{ reverse['@uuid'] }}.log { + roll_keep_for {{ generalSettings.LogAccessPlainKeep|default("10") }}d + } + } + {% endif %} + {% endif %} + {% set customCert = reverse.CustomCertificate|default("") %} + {% set dnsChallenge = reverse.DnsChallenge|default("0") %} + {{ tls_configuration(dnsProvider, dnsApiKey, customCert, dnsChallenge, dnsSecretApiKey, TlsDnsOptionalField1, TlsDnsOptionalField2, TlsDnsOptionalField3, TlsDnsOptionalField4, TlsDnsOptionalField5, TlsDnsOptionalField6) }} + + {% if not reverse.accesslist %} + {% set basicauth_uuids = reverse.basicauth %} + {{ basicauth_configuration(basicauth_uuids) }} + {% endif %} + + {% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %} + {% if subdomain.enabled|default("0") == "1" and subdomain.reverse == reverse['@uuid'] %} + @{{ subdomain['@uuid'] }} { + host {{ subdomain.FromDomain }}{% if subdomain.FromPort %}:{{ subdomain.FromPort }}{% endif %} + } + handle @{{ subdomain['@uuid'] }} { + + {% if not subdomain.accesslist %} + {% set subdomain_basicauth_uuids = subdomain.basicauth %} + {{ basicauth_configuration(subdomain_basicauth_uuids) }} + {% endif %} + + {% if subdomain.accesslist %} + {% set accesslist = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', subdomain.accesslist) | first %} + {{ access_list_configuration(accesslist, accesslist.accesslistInvert|default("0") == "1") }} + handle @{{ accesslist['@uuid'] }} { + + {% set subdomain_basicauth_uuids = subdomain.basicauth %} + {{ basicauth_configuration(subdomain_basicauth_uuids) }} + + {% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %} + {% for handle in subdomain_handles %} + {% if handle.enabled|default("0") == "1" and handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + {% for handle in subdomain_handles %} + {% if handle.enabled|default("0") == "1" and not handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + } + {% else %} + {% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %} + {% for handle in subdomain_handles %} + {% if handle.enabled|default("0") == "1" and handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + {% for handle in subdomain_handles %} + {% if handle.enabled|default("0") == "1" and not handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + {% endif %} + {% if Pischem.caddy.general.abort|default("0") == "1" %} + abort + {% endif %} + } + {% endif %} + {% endfor %} + + {% if reverse.accesslist %} + {% set accesslist = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', reverse.accesslist) | first %} + {{ access_list_configuration(accesslist, accesslist.accesslistInvert|default("0") == "1") }} + handle @{{ accesslist['@uuid'] }} { + + {% set basicauth_uuids = reverse.basicauth %} + {{ basicauth_configuration(basicauth_uuids) }} + + {% set wildcard_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('reverse', 'equalto', reverse['@uuid']) | selectattr('subdomain', 'undefined') | list %} + {% for handle in wildcard_handles %} + {% if handle.enabled|default("0") == "1" and handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + {% for handle in wildcard_handles %} + {% if handle.enabled|default("0") == "1" and not handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + } + {% else %} + {% set wildcard_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('reverse', 'equalto', reverse['@uuid']) | selectattr('subdomain', 'undefined') | list %} + {% for handle in wildcard_handles %} + {% if handle.enabled|default("0") == "1" and handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + {% for handle in wildcard_handles %} + {% if handle.enabled|default("0") == "1" and not handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + {% endif %} + {% if Pischem.caddy.general.abort|default("0") == "1" %} + abort + {% endif %} +} +{% endif %} +{% endfor %} + +import /usr/local/etc/caddy/caddy.d/*.conf diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/rc.conf.d/caddy b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/rc.conf.d/caddy new file mode 100644 index 000000000..503f7da6f --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/rc.conf.d/caddy @@ -0,0 +1,13 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +{% if helpers.exists('Pischem.caddy.general.enabled') %} + {%- set general_enabled = helpers.toList('Pischem.caddy.general.enabled') | first %} + {%- if general_enabled == '1' %} +caddy_enable="YES" +# Path to the Caddy setup script +caddy_setup="/usr/local/opnsense/scripts/OPNsense/Caddy/setup.sh" + {%- else %} +caddy_enable="NO" + {%- endif %} +{%- else %} +caddy_enable="NO" +{% endif %}