192 lines
8.5 KiB
Markdown
192 lines
8.5 KiB
Markdown
---
|
|
title: "Certbot : unprivileged Debian setup walkthrough"
|
|
date: 2023-10-24 21:30
|
|
last_modified_at: 2023-11-01 21:09
|
|
url: certbot-unprivileged-debian-setup-walkthrough
|
|
layout: post
|
|
category: Security
|
|
image: /img/blog/certbot-unprivileged-debian-setup-walkthrough_1.png
|
|
description: "Let's deploy and run these Python lines of code without any privilege !"
|
|
---
|
|
|
|
[![A missing blog post image](/img/blog/certbot-unprivileged-debian-setup-walkthrough_1.png)](/img/blog/certbot-unprivileged-debian-setup-walkthrough_1.png)
|
|
|
|
## Introduction
|
|
|
|
Some weeks ago, while I was upgrading my server operating system, I had to reboot the machine to load the new kernel. Unfortunately, the machine actually wasn't feeling well and decided _not_ to reboot. After contacting the data center support, it appeared the chassis had a critical hardware failure which prevented it from booting again.
|
|
|
|
This event taught me several things :
|
|
|
|
* reboot is not an innocuous operation (in my very case : IPMI fails very quickly after power on, and serial console was not reading anything interesting) ;
|
|
|
|
* hardware issues may have nothing to do with _current_ runtime (no kernel warning popped out recently) ;
|
|
|
|
* backups are primordial (you can't imagine how the last backup I've run some days before the upgrade reassured me during this period) ;
|
|
|
|
* SLA means _something_, including for IaaS (in my very case : probably due to a [blade containing multiple servers](https://en.wikipedia.org/wiki/Blade_server), data center operators couldn't access the chassis to get the disk out without impacting other clients. So they did not, according to the SLA).
|
|
|
|
On our "new" machine, I had to go through re-setup Web server(s) and, among other things, TLS certificates.
|
|
|
|
Do I really need to introduce you to [EFF](https://eff.org/)'s [Certbot](https://certbot.eff.org/), which you are very likely already using to obtained HTTPS certificates from [Let's Encrypt](https://letsencrypt.org/) ? I guess not, 'cause you wouldn't be reading this blog post if you know nothing about it.
|
|
|
|
> So what is the link between your server crash and Certbot ?
|
|
|
|
Well, this time I decided not to grant administrator privileges to this piece of software, and we'll see how we can achieve that.
|
|
|
|
## Installation
|
|
|
|
Official setup procedure [recommends](https://certbot.eff.org/instructions?ws=apache&os=debiantesting) to go through Canonical's [snapd](https://snapcraft.io/snapd) software to deploy Certbot, but I tend to reject these approaches, mostly when it's about running interpreted code (and not compiled C/C++ programs, which can require several libraries loaded at runtime, which can "justify" \[please note the quotes\] shipping tons of BLOBs to _ease_ deployment among heterogeneous systems).
|
|
|
|
[![A missing blog post image](/img/blog/certbot-unprivileged-debian-setup-walkthrough_2.png)](/img/blog/certbot-unprivileged-debian-setup-walkthrough_2.png)
|
|
|
|
As Certbot is still distributed through [PyPI](https://pypi.org/project/certbot/), we'll go this way.
|
|
|
|
{% highlight bash %}
|
|
apt install -y python3-venv
|
|
adduser --system certbot --home /opt/certbot
|
|
su - certbot -s /bin/bash
|
|
|
|
# As certbot user :
|
|
python3 -m venv venv && source venv/bin/activate
|
|
pip3 install -U pip wheel
|
|
pip3 install certbot
|
|
{% endhighlight %}
|
|
|
|
## Asking for a certificate
|
|
|
|
So there is two ways to ask for an HTTPS certificate : either Certbot spawns an HTTP Web server and directly responds to CA's http-01 challenge, or it could _write_ to an already "served" HTTP Web root.
|
|
|
|
### The standalone http-01 server way
|
|
|
|
The idea here is to make Certbot bind a local port > 1024, redirect new HTTP traffic to this port and let it directly respond to the http-01 challenge (as it were the actual Web server behind your domain name/IP address).
|
|
|
|
For a certificate that has been asked for the first time this way (as `certbot` user) :
|
|
|
|
{% highlight bash %}
|
|
venv/bin/certbot \
|
|
--work-dir=/opt/certbot \
|
|
--logs-dir=/opt/certbot/logs \
|
|
--config-dir=/opt/certbot/config \
|
|
certonly \
|
|
--http-01-address 127.0.0.1 \
|
|
--http-01-port 8080 \
|
|
-d "your.domain.name"
|
|
{% endhighlight %}
|
|
|
|
... I propose you below Bash renewal script (mainly the detailed steps to adapt with your setup) :
|
|
|
|
{% highlight bash %}
|
|
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
DOMAIN="your.domain.name"
|
|
WAN_IP_ADDRESS="your.wan.ip.address"
|
|
WAN_NETWORK_INTERFACE="eth0"
|
|
HTTP_01_LOCAL_ADDR="127.0.0.1"
|
|
HTTP_01_LOCAL_PORT=8080
|
|
|
|
# 1. Some firewall rules to DNAT and ACCEPT (new) HTTP traffic to local http-01 port
|
|
dnat_rule_handle="$(nft -ej insert rule ip nat prerouting ip daddr "$WAN_IP_ADDRESS" tcp dport http ct state new dnat to "${HTTP_01_LOCAL_ADDR}:${HTTP_01_LOCAL_PORT}" | grep -vE '^#' | jq -r .nftables[0].insert.rule.handle)"
|
|
filter_rule_handle="$(nft -ej insert rule inet filter input ip daddr "$HTTP_01_LOCAL_ADDR" tcp dport "$HTTP_01_LOCAL_PORT" ct state new accept | grep -vE '^#' | jq -r .nftables[0].insert.rule.handle)"
|
|
|
|
# 2. Allow DNAT to loopback
|
|
sysctl -q -w "net.ipv4.conf.${WAN_NETWORK_INTERFACE}.route_localnet=1"
|
|
|
|
# Renew the certificate using Certbot (`|| true` is required to allow it to fail, "for reasons")
|
|
su - certbot -s /bin/bash -c \
|
|
"/opt/certbot/venv/bin/certbot --work-dir=/opt/certbot --logs-dir=/opt/certbot/logs --config-dir=/opt/certbot/config renew -q" \
|
|
|| true
|
|
|
|
# 3. Disallow DNAT to loopback
|
|
sysctl -q -w "net.ipv4.conf.${WAN_NETWORK_INTERFACE}.route_localnet=0"
|
|
|
|
# 4. (situational) Install cryptographic materials where they need to be
|
|
cp "/opt/certbot/config/live/${DOMAIN}/fullchain.pem" /path/to/fullchain.pem
|
|
cp "/opt/certbot/config/live/${DOMAIN}/privkey.pem" /path/to/privkey.pem
|
|
|
|
# 5. (situational) Restart the service(s) to load the new certificate(s)
|
|
systemctl restart apache2.service
|
|
|
|
# 6. Delete our temporary firewall rules
|
|
nft delete rule inet filter input handle "$filter_rule_handle" || true
|
|
nft delete rule ip nat prerouting handle "$dnat_rule_handle" || true
|
|
{% endhighlight %}
|
|
|
|
For this script to work, I assume :
|
|
|
|
* `jq` is available on the system (used to parse `nft` JSON output) ;
|
|
|
|
* The firewall is managed through nftables ;
|
|
|
|
* (nftables) tables `ip nat` and `inet filter` exist ;
|
|
|
|
* (nftables) chains `prerouting` (`ip nat`) and `input` (`inet filter`) exist.
|
|
|
|
Note : http-01 server configuration is stored by Certbot so we don't have to specify `--http-01-*` arguments during renewal.
|
|
|
|
### The already "served" HTTP Web root
|
|
|
|
This is the method I'd prefer, as we don't have to play with firewall.
|
|
|
|
First, you will have to tweak your Web server configuration (i.e. the default VHOST) to :
|
|
|
|
1. Disable HTTPS redirection for `.well-known` URIs (if any) ;
|
|
|
|
2. Allows access to `.well-known/acme-challenge` Web root (if restricted).
|
|
|
|
Below, for instance, Apache httpd configuration :
|
|
|
|
{% highlight apache %}
|
|
<VirtualHost *:80>
|
|
ServerName your.domain.name
|
|
|
|
# Certbot
|
|
DocumentRoot /var/www
|
|
<Directory /var/www/.well-known/acme-challenge>
|
|
Require all granted
|
|
</Directory>
|
|
|
|
RewriteEngine on
|
|
RewriteCond %{REQUEST_URI} !^/\.well-known
|
|
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
|
|
</VirtualHost>
|
|
{% endhighlight %}
|
|
|
|
Now, you can run the following commands :
|
|
|
|
{% highlight bash %}
|
|
# Prepare the Web root
|
|
mkdir -p /var/www/.well-known/acme-challenge
|
|
chown www-data:www-data /var/www/.well-known
|
|
chown certbot:www-data /var/www/.well-known/acme-challenge
|
|
|
|
# Reload Apache httpd configuration
|
|
a2enmod rewrite
|
|
systemctl restart apache2.service
|
|
|
|
# Ask for a certificate
|
|
su - certbot -s /bin/bash -c \
|
|
"/opt/certbot/venv/bin/certbot --work-dir=/opt/certbot --logs-dir=/opt/certbot/logs --config-dir=/opt/certbot/config certonly --webroot-path=/var/www -d 'your.domain.name'
|
|
{% endhighlight %}
|
|
|
|
A typical renewal procedure would then be :
|
|
|
|
{% highlight bash %}
|
|
su - certbot -s /bin/bash -c \
|
|
"/opt/certbot/venv/bin/certbot --work-dir=/opt/certbot --logs-dir=/opt/certbot/logs --config-dir=/opt/certbot/config renew --deploy-hook 'touch certs_renewed' -q"
|
|
|
|
if [ -f /opt/certbot/certs_renewed ]; then
|
|
systemctl restart apache2.service
|
|
rm -f /opt/certbot/certs_renewed
|
|
fi
|
|
{% endhighlight %}
|
|
|
|
Note : Web root path configuration is stored by Certbot so we don't have to specify `--webroot-path` argument during renewal.
|
|
|
|
The trick with `--deploy-hook` is required as Certbot exits with status code `0` on "success" (i.e. either when zero, one or multiple certificates got renewed). @iquito's [workaround](https://github.com/certbot/certbot/issues/4090#issuecomment-282605558) is thus required here if we want to prevent unconditional Web server restart.
|
|
|
|
## Conclusion
|
|
|
|
Do backup. Try your restoration procedure. Encrypt the world. Get rid of unnecessary privileges. KISS.
|