Securing the Mirror: Native Let’s Encrypt SSL for My Local Package Cache

Estimated reading time: 3 minutes

Table of contents

In my previous post, I detailed how I built an automated Arch Linux (and Debian/Ubuntu) package cache. At the time, I mentioned: “Adding SSL logic directly to this script felt redundant for my use case.” Consequently, I relied on Nginx Proxy Manager (NPM) and Cloudflare for SSL termination with the use of Let’s Encrypt. As is my custom, I eventually decided to do things the “hard” way to ensure direct, secure access to the host.

I wanted to eliminate the dependency on an external proxy for internal traffic. Moving to native Let’s Encrypt SSL on the mirror host itself ensures that my pacoloco and apt-cacher-ng proxies are secured at the source. This is the (short) story of how I implemented a native certificate setup on Arch Linux.

Let's Encrypt

My Checklist for Native Mirror Security

I defined my requirements before touching the any configuration or installing any package.

  • Direct Issuance: The host must handle its own certificate lifecycle.
  • DNS-01 Challenge: Use the Cloudflare API to bypass port 80 requirements for validation.
  • Automated Renewal: A robust cron job to ensure the Let’s Encrypt certificate never expires.
  • Hardened Nginx: Full 80 to 443 redirection with optimized SSL paths.

The Solution: Native SSL with acme.sh

I chose acme.sh for this task. It is lightweight, has zero dependencies, and supports the Cloudflare DNS API out of the box.

1. Installing acme.sh

First, I installed the tool from source. This ensures I have all the latest DNS API hooks available.

curl -L https://github.com/acmesh-official/acme.sh/archive/master.tar.gz | tar xz
cd acme.sh-master
./acme.sh --install --force --accountemail [email protected]

2. Issuing the Let’s Encrypt Certificate

Next, I used the Cloudflare DNS-01 challenge to get my Let’s Encrypt certificate. This is significantly cleaner than the webroot challenge because it doesn’t require opening the firewall for validation.

export CF_Token='<YOUR_CLOUDFLARE_TOKEN>'
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --issue --dns dns_cf -d mirror.example.com</pre>

3. Nginx Configuration & Installation

I created a dedicated SSL directory in /etc/nginx/ssl/. Then, I used the --install-cert command to automate the deployment and reloading of Nginx.

# Create SSL directory
sudo mkdir -p /etc/nginx/ssl/
sudo chown -R myuser:myuser /etc/nginx/ssl/

# Install the certificate (no sudo needed since we own the directory)
  --key-file /etc/nginx/ssl/mirror.example.com.key 
  --fullchain-file /etc/nginx/ssl/mirror.example.com.fullchain 
  --reloadcmd 'sudo systemctl reload nginx'

Finally, I updated the /etc/nginx/nginx.conf file. I added a server block for port 443 and implemented a permanent redirect for port 80 traffic. This is what it looks like now:

user http;
worker_processes auto;
pid /var/run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    error_log   /var/log/nginx/error.log;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
    
    proxy_cache_path /var/cache/nginx/proxy_cache levels=1:2 keys_zone=aur_cache:10m max_size=5g inactive=60d use_temp_path=off;

    # --- HTTP: Redirect to HTTPS ---
    server {
        listen 80;
        server_name mirror.vanraaij.eu;
        return 301 https://$host$request_uri;
    }

    # --- HTTPS: Mirror Proxy ---
    server {
        listen 443 ssl;
        http2 on;
        server_name mirror.example.com;

        ssl_certificate /etc/nginx/ssl/mirror.example.com.fullchain;
        ssl_certificate_key /etc/nginx/ssl/mirror.example.com.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        auth_basic "Private Pacman Proxy";
        auth_basic_user_file /etc/nginx/.htpasswd;

        proxy_cache aur_cache;
        proxy_cache_valid 200 302 7d;
        proxy_cache_valid 404 1m;

        location ~ ^/pacman/.*\.db(\.sig)?$ {
            proxy_no_cache 1;
            proxy_cache_bypass 1;
            rewrite ^/pacman/(.*)$ /$1 break;
            proxy_pass http://127.0.0.1:9129;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /apt/ {
            proxy_pass http://127.0.0.1:3142/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            auth_basic "Private Repo Proxy";
            auth_basic_user_file /etc/nginx/.htpasswd;
        }

        location /pacman/ {
            proxy_buffering off;
            proxy_hide_header Content-Length;
            proxy_pass http://127.0.0.1:9129/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

4. Automating the Renewal (The Sudoers Catch)

acme.sh automatically installs a cron job to handle certificate renewals. However, there is a catch. The script runs as our standard user (myuser), but the --reloadcmd we specified requires sudo privileges to reload Nginx.
If we leave it as is, the automated renewal will succeed, but Nginx will fail to reload because the cron job cannot enter a sudo password.
To fix this, I created a specific drop-in rule in the `/etc/sudoers.d/` directory to allow `myuser` to reload Nginx without a password:

cat << 'EOF' | sudo tee /etc/sudoers.d/myuser > /dev/null
myuser ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx
EOF

Final Thoughts

The migration was seamless. I verified the setup by running sudo pacman -Syu on my clients. Everything now flows through a secure HTTPS connection directly to the mirror host at 192.168.0.102.

Furthermore, the automated renewal is already active in the crontab. My mirror host is now a standalone, secure fortress that no longer relies on an external proxy for internal integrity.


Buy me a coffee 🙂
If you found this post helpful, informative, or if it saved or made you some money, consider buying me a coffee. Your support means a lot and motivates me to keep writing.
You can do so via bunq.me (bunq, iDeal, Bankcontact and Credit- or Debit cards) or PayPal (PayPal and Credit- or Debit cards). Thank you!

Disclaimer
The blog posts, guides, and scripts provided on this website are for informational and educational purposes only. They are provided “as is” and without any warranty of any kind, either express or implied, including, but not limited to, the implied warranties of merchantability, fitness for a particular purpose, or non-infringement.
By using the information or scripts from this blog, you agree that I am not liable for any direct, indirect, incidental, consequential, or any other damages or losses arising from the use of or inability to use the information, scripts, or instructions contained herein. You assume full responsibility for any and all risks associated with the use of this content.
The blog posts, guides, and pages may contain referral/affiliate links. If you make a purchase through these links, I may receive a commission at no additional cost to you.

 
Posted in Arch Linux, Linux, Linux Tutorials, Network Security, System AdministrationTags:
Write a comment