From c035f62b41804841a7c5e405c224bd450575a40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Wed, 10 Apr 2024 18:34:58 +0200 Subject: [PATCH] WIP: Blog: ACME --- .../acme_infrastructure_in_private_lan.md | 69 +++++++++++++++++++ content/blog/test01.md | 10 --- content/blog/test02.md | 10 --- content/blog/test_long.md | 9 --- scss/_blog.scss | 8 +++ scss/_params.scss | 2 + static/css/style.css | 7 ++ 7 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 content/blog/acme_infrastructure_in_private_lan.md delete mode 100644 content/blog/test01.md delete mode 100644 content/blog/test02.md delete mode 100644 content/blog/test_long.md diff --git a/content/blog/acme_infrastructure_in_private_lan.md b/content/blog/acme_infrastructure_in_private_lan.md new file mode 100644 index 0000000..c125675 --- /dev/null +++ b/content/blog/acme_infrastructure_in_private_lan.md @@ -0,0 +1,69 @@ +--- +title: "Infrastructure for ACME (LetsEncrypt) certificates in a private LAN" +date: 2024-04-10 +draft: false +'blog/tags': +- sysadmin +- Échirolles +--- + +[LetsEncrypt](https://letsencrypt.org) --- the non-profit certification +authority that certifies a vast portion of the web --- doesn't really need +introduction anymore. They provide free SSL certificates, and even better, they +do so using their standardized automated protocol, ACME +([RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555)). + +When a client requests a certificate through ACME for a domain, it will be +handed a token --- a random string --- by the server. This token (the +challenge) must be temporarily served by the client to prove that it indeed +controls the domain. This automated validation uses either `http-01` or +`dns-01` challenges. The former requires the client to serve the token via http +at a certain URL. The latter requires the client to register a `TXT` DNS entry +under the domain bearing the token. The `http-01` process is clearly easier to +implement, and makes ACME straightforward in many cases. + +Assume, however, that the machine trying to obtain a certificate is in a +private LAN --- it has no public IP address, or should remain completely +firewalled from the Internet, including the ACME server. In this case, the +`http-01` challenge cannot be used, and `dns-01` must be used instead. Many +articles online give instructions on how to make Certbot or other ACME clients +update DNS records. However, I am really not comfortable with giving all of my +applicative servers write-access to my DNS zones. + +## ACME NS challenger + +Our solution, instead, is to leave the ACME client running on the applicative +server (we use [Dehydrated](https://github.com/dehydrated-io/dehydrated)), and +to implement a simple API on the DNS server to allow applicative servers to +update only what they need to be able to update. + +On the DNS server, a simple Flask-based webserver listens on the internal +network. It has two API endpoints: one to create an ACME challenge for a domain +(`PUT` request), one to mark a previously-created challenge as obsolete +(`DELETE` request). + +Each request is validated in two ways: +* it must come from an IP address which resolves to the domain to be updated + --- with some configurable exceptions for eg. domains with addresses in + isolated networks; +* it must bear a challenge, which is the HMAC of the current time and a shared + secret between the DNS and applicative server (distributed out-of-band + earlier). The DNS server thus identifies which token is allowed to update + which domain. + +An intermediate directory contains all the currently deployed challenges. The +`PUT` request for domain X generates a DNS zone file for X under the +intermediate directory; a `DELETE` request thus only needs to delete this file. +Finally, after a `PUT` request, all domain-specific zone files are gathered +under a single file, with the Python equivalent of +`cat intermediate_dir/*.zone > acme.zone`. A `DELETE` request, however, only +deletes the intermediate file: to avoid generating too many zone updates, the +global zone file will only be updated upon the next `PUT`, or upon scheduled +garbage collection. + +Finally, the generated `acme.zone` file is included in the global DNS +configuration file. + +## Deployment in practice + + diff --git a/content/blog/test01.md b/content/blog/test01.md deleted file mode 100644 index 9481abd..0000000 --- a/content/blog/test01.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Test 1 -date: 2023-04-01 -draft: true -"blog/tags": -- test -- misc ---- - -Bonjour, ceci est un test. diff --git a/content/blog/test02.md b/content/blog/test02.md deleted file mode 100644 index e37c6e2..0000000 --- a/content/blog/test02.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Test 2 -date: 2023-05-02 -draft: true -"blog/tags": -- test -- second ---- - -Bonjour, ceci est également un test. diff --git a/content/blog/test_long.md b/content/blog/test_long.md deleted file mode 100644 index 612be5d..0000000 --- a/content/blog/test_long.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Ceci, par rapport aux autres, est un test avec un titre d'une longueur fort surprenante. -date: 2023-05-12 -draft: true -"blog/tags": -- test ---- - -Bonjour, ceci est également un test. diff --git a/scss/_blog.scss b/scss/_blog.scss index 10b74b9..10c6965 100644 --- a/scss/_blog.scss +++ b/scss/_blog.scss @@ -97,4 +97,12 @@ padding-left: 15px; margin-left: 25px; } + + :not(pre) > code { + background-color: $icode_bg_color; + padding: 2px 4px; + border-radius: 4px; + font-size: 85%; + color: $icode_fg_color; + } } diff --git a/scss/_params.scss b/scss/_params.scss index c1211ec..8fec1bb 100644 --- a/scss/_params.scss +++ b/scss/_params.scss @@ -5,6 +5,8 @@ $link_color: #07a; $fg_color: #555; $fg_color_light: #555555bb; $head_bg_color: #060033; +$icode_fg_color: $fg_color; +$icode_bg_color: #e1e1e1; $resp_small: 1350px; $resp_vsmall: 1000px; diff --git a/static/css/style.css b/static/css/style.css index 81b0f7f..13d58f2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -69,6 +69,13 @@ padding-left: 15px; margin-left: 25px; } +.blog :not(pre) > code { + background-color: #e1e1e1; + padding: 2px 4px; + border-radius: 4px; + font-size: 85%; + color: #555; } + html { background-color: white; }