Adds configuration, scripts, and documentation for the LE TXT challenge

This commit is contained in:
Maximilian Kratz 2024-04-21 09:13:01 +02:00
parent 056076e0c0
commit 5a666b3469
9 changed files with 127 additions and 7 deletions

View file

@ -1,16 +1,23 @@
# CoreDNS-DynDNS
This repository holds some basic configurations to enable dynamic DNS updates in an authorative DNS-Server running [CoreDNS](https://github.com/coredns/coredns) without implementing a custom plugin.
Therefore, this repsoitory "simulates" a [DynDNSv2](https://stackoverflow.com/questions/54039095/dyndns2-protocol-specification) endpoint.
Therefore, this repsoitory "simulates" a [DynDNSv2](https://stackoverflow.com/questions/54039095/dyndns2-protocol-specification) endpoint.
Available functions:
- Change an `A` record via DynDNSv2.
- Update a `TXT` record for the [Let's Encrypt DNS challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) via a [Cerbot](https://certbot.eff.org/) script.
Keep in mind that this is kind of a blueprint and not necessary production-ready.
## How does it work?
## DynDNS A record update
### How does it work?
CoreDNS is able to reread configuration and zone files on changes.
This project provides a quite simple way to update such a zone file via a [webhook](https://github.com/adnanh/webhook).
Most *decent* routers allow to specify a custom URL with basic auth to send IP updates to.
The webhook provides such a URL and triggers a sh script that updates the dynamic zone file and boom - your dynamic IP updates are done.
The webhook provides such a URL and triggers an sh script that updates the dynamic zone file and boom - your dynamic IP updates are done.
**In short (step-by-step):**
1. The client receives a new dynamic IP address.
@ -24,7 +31,7 @@ The sh script checks if the provided parameter is a valid IPv4 address using a r
Currently, the updating of IPv6 addresses is **not** supported.
## Configuration
### Configuration
A [Docker-Compose](https://docs.docker.com/compose/) stack is used for the example configuration.
Feel free to adapt the needed steps to, e.g., a native installation (with binaries) (although Docker-Compose is running just fine for my personal setup).
@ -46,7 +53,7 @@ Hint: Mostly, you want to use the stack behind a reverse proxy such as nginx sec
For an easy to use reverse proxy for Docker containers, check out [nginx-proxy](https://github.com/nginx-proxy/nginx-proxy) with its [config examples](https://github.com/nginx-proxy/acme-companion/blob/main/docs/Docker-Compose.md).
### OPNsense client example
#### OPNsense client example
This example can easily be used to update a dynamic zone with your [OPNsense](https://opnsense.org/) router.
@ -66,3 +73,31 @@ Configure the following settings:
| *Force SSL* | Checked | Uses HTTPS instead of HTTP |
| *Interface to monitor* | E.g., *WAN* | Interface on which the dynamic IP occurs |
| *Description* | | - |
## Let's Encrypt DNS challenge
Additionally to the previous mentioned updating of A records, this stack also provides a function to update a TXT record for a Let's Encrypt DNS challenge.
This means you can use it to include a TXT record that Certbot needs to update a Let's Encrypt certificate on a server that may not have a publicly available web server on port 80 + 443.
### How does it work?
The core information is explained in the section of updating A records above.
Let's Encrypt steps:
1. Certbot requests a new certificate. The Let's Encrypt servers request to update the TXT record with value "ABC".
1. Certbot triggers the [le.sh](./scripts/certbot/le.sh) script with value "ABC".
1. The script sends a webhook with parameter "ABC" to http://$nameserver/nic/le
1. The script [cng.sh](./scripts/cng.sh) updates the dynamic zone file (including "ABC" as TXT record), updates the serial, and saves it to disk.
1. CoreDNS automatically reloads the zone file and serves the new IP address.
1. [le.sh](./scripts/certbot/le.sh) waits 60s to ensure the updated zone is available.
1. The Let's Encrypt servers can now validate the TXT record and give the new certificate to Certbot.
**Relevant files**:
| File | Purpose |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| [le.sh](./scripts/certbot/le.sh) | Certbot script that can provide the necessary call to this stack. |
| [cng.sh](./scripts/cng.sh) | Script that updates the TXT record within the dynamic zone file (analogously to the A record script explained above). |
| [htpasswd_le](./config/dynamic/htpasswd_le) | Contains the authentification user/pw combination for the `cng.sh` script. |
| [db.example.com.dyn.template](./zones/example.com/db.example.com.dyn.template) | Contains the necessary A record as well as the TXT record to update. |
| [default.conf](./config/dynamic/default.conf) | nginx configuration to pass calls to respective webhooks. |
| [webhook.json](./config/dynamic/webhook.json) | Contains the webhook configuration. |

View file

@ -9,6 +9,12 @@ server {
listen 80;
server_name localhost;
location /nic/le {
auth_basic "Restricted API";
auth_basic_user_file /etc/nginx/conf.d/htpasswd_le;
proxy_pass http://webhook/hooks/le;
}
location /nic {
auth_basic "Restricted API";
auth_basic_user_file /etc/nginx/conf.d/htpasswd;

View file

@ -0,0 +1 @@
example:$1$Qbub^M\U$3601.7bQwZws2mUa7RHWf1

View file

@ -14,5 +14,17 @@
"name": "hostname"
}
]
},
{
"id": "le",
"execute-command": "/app/cng.sh",
"include-command-output-in-response": true,
"pass-arguments-to-command":
[
{
"source": "query",
"name": "value"
}
]
}
]

View file

@ -3,7 +3,7 @@ $TTL 3600
; SOA Record
@ 3600 IN SOA ns1.example.com. hostmaster.example.com. (
2021012220 ; serial
2021012221 ; serial
7200 ; refresh (7200 = 2 hours)
3600 ; retry (3600 = 1 hour)
1209600 ; expire (1209600 = 2 weeks)
@ -21,3 +21,7 @@ dyn 3600 IN NS ns3.example.com.
; A / AAAA Records
* IN A 192.0.2.1
@ IN A 192.0.2.1
; Lets Encrypt challenge
;_acme-challenge IN TXT "a"
_acme-challenge IN CNAME _acme-challenge.dyn.example.com.

View file

@ -3,7 +3,7 @@ $TTL 300
; SOA Record
@ 3600 IN SOA ns1.example.com. hostmaster.example.com. (
2021012220; serial
2021012221; serial
7200 ; refresh (7200 = 2 hours)
3600 ; retry (3600 = 1 hour)
1209600 ; expire (1209600 = 2 weeks)
@ -17,3 +17,6 @@ $TTL 300
; A / AAAA Records
@ IN A 192.0.2.2
; Lets Encrypt challenge
_acme-challenge IN TXT "a"

View file

@ -23,6 +23,7 @@ services:
volumes:
- './config/dynamic/default.conf:/etc/nginx/conf.d/default.conf:ro'
- './config/dynamic/htpasswd:/etc/nginx/conf.d/htpasswd:ro'
- './config/dynamic/htpasswd_le:/etc/nginx/conf.d/htpasswd_le:ro'
# webhook that triggers ip address update
webhook:
@ -32,4 +33,5 @@ services:
volumes:
- './config/dynamic/webhook.json:/etc/webhook/hooks.json:ro'
- './scripts/dyn.sh:/app/dyn.sh:ro'
- './scripts/cng.sh:/app/cng.sh:ro'
- './config/zones/example.com/db.example.com.dyn:/zonefile'

14
scripts/certbot/le.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
set -e
# Config
USER="TODO"
PW="TODO"
HOST="http://dyn.ns1.example.com"
# Hook
curl -u "$USER:$PW" $HOST/nic/le\?value\="$CERTBOT_VALIDATION"
# Wait to let DNS propagate
sleep 60

43
scripts/cng.sh Executable file
View file

@ -0,0 +1,43 @@
#!/bin/sh
set -e
# Get VALUE from parameter
VAL=$1
# Check if provided IP parameter was empty
if [ -z "$VAL" ]
then
echo "badsys"
exit 0;
fi
# Get previous TXT from zone file
TXT_OLD=$(grep '_acme-challenge IN TXT "' /zonefile_le | cut -d\ -f4)
# Replace old val from zonefile by new val, use tee instead of sed -i
# because Docker does not permit the overwriting of inodes.
sed "s/_acme-challenge IN TXT \".*/_acme-challenge IN TXT \"${VAL}\"/" /zonefile_le > /zonefile_le.tmp
# Get old serial from zonefile and set value to current date/time stamp
SERIAL_OLD=$(cat /zonefile_le.tmp | grep "; serial" | tr -dc '0-9')
SERIAL_NEW=$(date +%y%m%d%H%M)
# Ensure that new serial is always larger than previous one
# (This could happen if there is more than one update per minute)
if [ "$SERIAL_OLD" -ge "$SERIAL_NEW" ]; then
SERIAL_NEW=$((SERIAL_OLD+1))
fi
# Replace old serial with new value in temp zonefile
sed -i "s/$SERIAL_OLD/$SERIAL_NEW/" /zonefile_le.tmp
# Overwrite old zonefile by temp zonefile
cat /zonefile_le.tmp > /zonefile_le
if [[ "$TXT_OLD" == "$VAL" ]]
then
echo "nochg"
else
echo "good"
fi