<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Caddy — LowEndSpirit</title>
        <link>https://staging.lowendspirit.com/index.php?p=/</link>
        <pubDate>Sat, 11 Apr 2026 22:52:14 +0000</pubDate>
        <language>en</language>
            <description>Caddy — LowEndSpirit</description>
    <atom:link href="https://staging.lowendspirit.com/index.php?p=/discussions/tagged/caddy/feed.rss" rel="self" type="application/rss+xml"/>
    <item>
        <title>How to Host a Website in Oracle Cloud Free Tier</title>
        <link>https://staging.lowendspirit.com/index.php?p=/discussion/2339/how-to-host-a-website-in-oracle-cloud-free-tier</link>
        <pubDate>Tue, 29 Dec 2020 14:46:26 +0000</pubDate>
        <category>Technical</category>
        <dc:creator>yoursunny</dc:creator>
        <guid isPermaLink="false">2339@/index.php?p=/discussions</guid>
        <description><![CDATA[<blockquote><div>
  <p>This post is originally published on yoursunny.com blog <a href="https://yoursunny.com/t/2020/OracleCloud-website/" rel="nofollow">https://yoursunny.com/t/2020/OracleCloud-website/</a></p>
</div></blockquote>

<p><strong>Oracle Cloud</strong> is a cloud computing service offered by Oracle Corporation.<br />
Oracle Cloud has a generous <a rel="nofollow" href="https://www.oracle.com/cloud/free/">free tier</a> that offers two "always free" Micro instances with the following <a rel="nofollow" href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm">specification</a>:</p>

<ul>
<li>KVM virtualization</li>
<li>1/8 CPU cores (AMD EPYC 7551)</li>
<li>1GB memory</li>
<li>47GB disk storage</li>
<li>1 IPv4 address</li>
<li>up to 32 IPv6 addresses</li>
<li>50Mbps Internet bandwidth</li>
</ul>

<p>I <a rel="nofollow" href="https://www.oracle.com/cloud/free/">signed up</a> for Oracle Cloud, so that I can have some more free computing resources to play with.<br />
The sign-up procedure requires a credit card for identity confirmation purpose, but the credit card will not be charged.<br />
During sign-up, there's a choice of <em>home region</em>, which determines the location of VM instances; once selected, it cannot be changed in the future.</p>

<p>A common use case for a virtual machine is to host a website.<br />
Due to the firewalls, hosting a website on Oracle Cloud needs a few more steps.<br />
Here's exactly how to deploy a website in a Oracle Cloud Free Tier VM instance.</p>

<p>UPDATED 2022-01-27:<br />
Oracle Cloud now supports IPv6.<br />
Instructions are updated to enable IPv6 on the web server.</p>

<h2>Create a VM Instance</h2>

<p>Each Oracle Cloud account is eligible for two <em>Always Free</em> Micro instances.<br />
To create a VM, <a rel="nofollow" href="https://cloud.oracle.com/">sign in to the Oracle Cloud console</a>, in "Launch Resources" section click <strong>Create a VM instance</strong>.<br />
This takes us to the "Create Compute Instance" page.</p>

<p>In "Image and shape" section, select <strong>VM.Standard.E2.1.Micro</strong> shape and <strong>Canonical Ubuntu 20.04</strong> image.<br />
Do not use the "Canonical Ubuntu 20.04 Minimal" image.</p>

<p><img src="https://yoursunny.com/t/2020/OracleCloud-website/ImageShape.png" alt="Create Compute Instance - Image and shape" /></p>

<p>In "Configure network" section, select <strong>Create new virtual cloud network</strong>, and keep other options at their default values.</p>

<p>In "Add SSH keys" section, select <strong>Paste public keys</strong>, and paste your SSH public key in the text box below.<br />
If you do not have a SSH public key, <a rel="nofollow" href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key">follow this guide</a> to generate one.</p>

<p>Finally, click the <strong>Create</strong> button to create the compute VM instance.<br />
Within a few seconds, you should see the "Instance Details" page.</p>

<p><img src="https://yoursunny.com/t/2020/OracleCloud-website/InstanceDetails.png" alt="Instance Details" /></p>

<p>You can SSH into the VM instance using the public IP address and username displayed in the "Instance Access" section.</p>

<h2>Enable IPv6</h2>

<p>In this year, it is important for websites to support IPv6.<br />
Today, most cellular networks are IPv6 only.<br />
By enabling IPv6, it enables mobile users to access your website more efficiently, because their sessions do not need to go through IPv4 address translation proxies.</p>

<p>Oracle Cloud compute VM instances are initially assigned with only an IPv4 address.<br />
There are 5 steps for enabling IPv6 on the VM:</p>

<ol>
<li>Assign IPv6 CIDR block to the <em>Virtual Cloud Network</em>.</li>
<li>Assign IPv6 CIDR block to the <em>Subnet</em>.</li>
<li>Configure IPv6 <em>Route Rule</em>.</li>
<li>Assign IPv6 Address to the compute VM's <em>VNIC</em>.</li>
<li>Configure firewall rules.</li>
</ol>

<p>We will perform steps 1~4 now, and do step 5 a little later (in "Configure Ingress and Egress Rules" section).</p>

<p>Looking at the "Instance Details" page, click the link next to "Virtual cloud network".<br />
This takes us to the "Virtual Cloud Network Details" page.</p>

<p>Select "CIDR Blocks" tab on the left side "Resources" menu, and then click <strong>Add IPv6 CIDR Block</strong> button.<br />
You will be asked to confirm that you want to enable IPv6.<br />
When you click <strong>Confirm</strong>, a /56 block of 4,722,366,482,869,645,213,696 IPv6 addresses are automatically allocated to your <em>Virtual Cloud Network</em> (IPv6 step 1).</p>

<p><img src="https://yoursunny.com/t/2020/OracleCloud-website/CIDR.png" alt="CIDR Blocks" /></p>

<p>Select "Subnets" tab on the left side "Resources" menu.<br />
You should see an existing <em>Subnet</em>.<br />
It would have an IPv4 CIDR Block, but the "IPv6 CIDR Block" column is blank.<br />
Click the <strong>⋮</strong> button in the rightmost column, and select "Edit" in the dropdown menu.<br />
Then, check <strong>Enable IPv6 CIDR Block</strong> box, enter two hexadecimal digits (such as <code>00</code>) in the box just before "::/64", and click <strong>Save Changes</strong> (IPv6 step 2).</p>

<p><img src="https://yoursunny.com/t/2020/OracleCloud-website/IPv6CIDR.png" alt="IPv6 CIDR Block" /></p>

<p>Select "Route Tables" tab on the left side "Resources" menu, and then click <strong>Default Route Table</strong> link.<br />
This takes us to the "Route Table Details" pages.<br />
In the "Route Rules" tables, we can see that there's a route rule for destination <code>0.0.0.0/0</code> that targets the Internet gateway, which allows IPv4 packets to reach the Internet.<br />
We need a similar route rule for IPv6.<br />
Click <strong>Add Route Rules</strong> button.<br />
In the popup dialog, enter the following:</p>

<ul>
<li>Protocol Version: IPv6</li>
<li>Target Type: Internet Gateway</li>
<li>Destination CIDR Block: <code>::/0</code></li>
<li>Target Internet Gateway: Internet gateway vcn-…</li>
</ul>

<p>Then click <strong>Add Route Rules</strong> button (IPv6 step 3).</p>

<p>Finally, go back to the "Instance Details" page of the compute VM instance.<br />
To find that page, you can type "Instances" into the search bar and select "Services - Instances (Compute)" in the results.</p>

<p>Select "Attached VNICs" tab on the left side "Resources" menu, and then click the link next to <strong>(Primary VNIC)</strong>.<br />
This opens the "VNIC Details".<br />
Select "IPv6 Addresses" tab on the left side "Resources" menu, and click <strong>Assign IPv6 Address</strong> button.<br />
In the popup dialog, click <strong>Assign</strong> button to get a random IPv6 address (IPv6 step 4).</p>

<h2>DNS</h2>

<p>Now that we have the IP addresses assigned, it is a good time to add DNS records to our compute VM instance, so that we can activate HTTPS later.<br />
Two DNS records are required: an <em>A record</em> for the public IPv4 address and an <em>AAAA record</em> for the public IPv6 address.<br />
You can find both addresses in the "VNIC Details" page, as described above.</p>

<p><img src="https://yoursunny.com/t/2020/OracleCloud-website/DNS.png" alt="DNS record" /></p>

<h2>Configure Ingress and Egress Rules</h2>

<p>Oracle Cloud has a strict firewall that, by default, only allows SSH access.<br />
In order to host a website, it is necessary to configure the firewall so that it allows HTTP traffic.</p>

<p>To access the firewall configuration page, click the "subnet" name in "Primary VNIC" section of "Instance Details" page.<br />
Then, on "Subnet Details" page, click "Default Security List for …" in "Security Lists" section.<br />
Click <strong>Add Ingress Rules</strong> button, and enter these four rules in the popup dialog:</p>

<ul>
<li><p>Allow HTTP/1.1 and HTTP/2 (IPv4)</p>

<ul>
<li>stateless: no</li>
<li>source CIDR: <code>0.0.0.0/0</code></li>
<li>IP protocol: <code>TCP</code></li>
<li>destination port range: <code>80,443</code></li>
</ul></li>
<li><p>Allow HTTP/1.1 and HTTP/2 (IPv6)</p>

<ul>
<li>stateless: no</li>
<li>source CIDR: <code>::/0</code></li>
<li>IP protocol: <code>TCP</code></li>
<li>destination port range: <code>80,443</code></li>
</ul></li>
<li><p>Allow HTTP/3 (IPv4)</p>

<ul>
<li>stateless: no</li>
<li>source CIDR: <code>0.0.0.0/0</code></li>
<li>IP protocol: <code>UDP</code></li>
<li>destination port range: <code>443</code></li>
</ul></li>
<li><p>Allow HTTP/3 (IPv6)</p>

<ul>
<li>stateless: no</li>
<li>source CIDR: <code>::/0</code></li>
<li>IP protocol: <code>UDP</code></li>
<li>destination port range: <code>443</code></li>
</ul></li>
</ul>

<p>After that, you should see the following ingress rules in the table:</p>

<p><img src="https://yoursunny.com/t/2020/OracleCloud-website/IngressRules.png" alt="Virtual Cloud Networks - Ingress Rules" /></p>

<p>You should also add an IPv6 egress rule, so that the VM instance can reach Internet resources over IPv6.<br />
To do that, select "Egress Rules" tab on the left side "Resources" menu.<br />
Click <strong>Add Egress Rules</strong> button, and enter the following rule in the popup dialog (IPv6 step 5):</p>

<ul>
<li>Allow IPv6 Internet access

<ul>
<li>stateless: no</li>
<li>destination CIDR: <code>::/0</code></li>
<li>IP protocol: <code>All Protocols</code></li>
</ul></li>
</ul>

<h2>Install HTTP Server</h2>

<p>With the firewall rules in place, we are ready to install an HTTP server.<br />
In this guide, I'm installing <a rel="nofollow" href="https://caddyserver.com/">Caddy HTTP server</a> along with PHP-FPM.<br />
They can be installed from <a rel="nofollow" href="https://caddyserver.com/docs/download#debian-ubuntu-raspbian">Caddy package repository</a> and <a rel="nofollow" href="https://launchpad.net/~ondrej/+archive/ubuntu/php">ondrej/php PPA</a> respectively.</p>

<pre><code># see "Caddy package repository" link above for how to add Caddy APT repository
sudo add-apt-repository ppa:ondrej/php
sudo apt install caddy php8.1-fpm
</code></pre>

<p>Before we can start the HTTP server, there's one more firewall to configure: the local <em>iptables</em>.<br />
Oracle Cloud not only has an external firewall at subnet level, but also blocks traffic in iptables INPUT chain.<br />
We can setup a systemd service to insert iptables rules before Caddy starts:</p>

<pre><code>sudoedit /etc/systemd/system/caddy-iptables.service
  (paste the caddy-iptables.service content)

sudo systemctl daemon-reload
sudo systemctl enable --now caddy-iptables
</code></pre>

<p>The systemd unit file <code>caddy-iptables.service</code> should have the following content:</p>

<pre><code>[Unit]
Description=Firewall rules for Caddy
Before=caddy.service

[Service]
ExecStartPre=+/usr/sbin/iptables -I INPUT -p tcp --dport 80 -j ACCEPT
ExecStartPre=+/usr/sbin/iptables -I INPUT -p tcp --dport 443 -j ACCEPT
ExecStartPre=+/usr/sbin/iptables -I INPUT -p udp --dport 443 -j ACCEPT
ExecStartPre=+/usr/sbin/ip6tables -I INPUT -p tcp --dport 80 -j ACCEPT
ExecStartPre=+/usr/sbin/ip6tables -I INPUT -p tcp --dport 443 -j ACCEPT
ExecStartPre=+/usr/sbin/ip6tables -I INPUT -p udp --dport 443 -j ACCEPT
ExecStart=true
RemainAfterExit=yes
ExecStopPost=+/usr/sbin/iptables -D INPUT -p tcp --dport 80 -j ACCEPT
ExecStopPost=+/usr/sbin/iptables -D INPUT -p tcp --dport 443 -j ACCEPT
ExecStopPost=+/usr/sbin/iptables -D INPUT -p udp --dport 443 -j ACCEPT
ExecStopPost=+/usr/sbin/ip6tables -D INPUT -p tcp --dport 80 -j ACCEPT
ExecStopPost=+/usr/sbin/ip6tables -D INPUT -p tcp --dport 443 -j ACCEPT
ExecStopPost=+/usr/sbin/ip6tables -D INPUT -p udp --dport 443 -j ACCEPT

[Install]
RequiredBy=caddy.service
</code></pre>

<p>Upload your website content, and make sure the <code>www-data</code> group can access them.<br />
In this example, I'll create two simple files:</p>

<pre><code>sudo mkdir -p /var/www/html
echo '&lt;h1&gt;hello&lt;/h1&gt;' | sudo tee /var/www/html/index.html
echo '&lt;?php phpinfo(); ?&gt;' | sudo tee /var/www/html/phpinfo.php
sudo chgrp -R www-data /var/www/html
</code></pre>

<p>Edit the Caddyfile (<code>/etc/caddy/Caddyfile</code>), paste the following:<br />
(change the domain name and root directory as appropriate)</p>

<pre><code>{
  servers {
    protocol {
      experimental_http3
    }
  }
}

https://demo.example.com {
  root * /var/www/html
  file_server
  php_fastcgi unix//run/php/php8.1-fpm.sock

  header {
    Strict-Transport-Security max-age=2592000
    X-Frame-Options SAMEORIGIN
    X-Content-Type-Options nosniff
    Referrer-Policy no-referrer-when-downgrade
  }
}
</code></pre>

<p>Finally, restart the webserver for the settings to take effect:</p>

<pre><code>sudo systemctl restart caddy
sudo systemctl restart php8.1-fpm
</code></pre>

<h2>Test the Website</h2>

<p>To confirm everything is working, we can visit the index page <code>https://demo.example.com/</code> and the PHP script <code>https://demo.example.com/phpinfo.php</code> in the browser.</p>

<p>Then, we use the <a rel="nofollow" href="https://curl.se/">curl</a> command line tool (on a different machine) to check that:</p>

<ul>
<li>HTTP-to-HTTPS redirect is working properly.</li>
<li>The website is served over HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3.</li>
<li>The server is accessible over both IPv4 and IPv6.</li>
</ul>

<pre><code>$ curl -4 --http1.0 -I http://demo.example.com/
HTTP/1.0 308 Permanent Redirect
Connection: close
Location: https://demo.example.com/
Server: Caddy
Date: Fri, 28 Jan 2022 02:16:43 GMT

$ curl -6 --http1.0 -I http://demo.example.com/
HTTP/1.0 308 Permanent Redirect
Connection: close
Location: https://demo.example.com/
Server: Caddy
Date: Fri, 28 Jan 2022 02:16:43 GMT

$ curl -4 --http1.1 -I http://demo.example.com/
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://demo.example.com/
Server: Caddy
Date: Fri, 28 Jan 2022 02:16:44 GMT

$ curl -6 --http1.1 -I http://demo.example.com/
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://demo.example.com/
Server: Caddy
Date: Fri, 28 Jan 2022 02:16:44 GMT

$ curl -4 --http1.1 -I https://demo.example.com/
HTTP/1.1 200 OK
Accept-Ranges: bytes
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Length: 15
Content-Type: text/html; charset=utf-8
Etag: "r6edtff"
Last-Modified: Fri, 28 Jan 2022 02:05:39 GMT
Referrer-Policy: no-referrer-when-downgrade
Server: Caddy
Strict-Transport-Security: max-age=2592000
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Date: Fri, 28 Jan 2022 02:16:45 GMT

$ curl -6 --http1.1 -I https://demo.example.com/
HTTP/1.1 200 OK
Accept-Ranges: bytes
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Length: 15
Content-Type: text/html; charset=utf-8
Etag: "r6edtff"
Last-Modified: Fri, 28 Jan 2022 02:05:39 GMT
Referrer-Policy: no-referrer-when-downgrade
Server: Caddy
Strict-Transport-Security: max-age=2592000
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Date: Fri, 28 Jan 2022 02:16:45 GMT

$ curl -4 --http2 -I https://demo.example.com/
HTTP/2 200
accept-ranges: bytes
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
content-type: text/html; charset=utf-8
etag: "r6edtff"
last-modified: Fri, 28 Jan 2022 02:05:39 GMT
referrer-policy: no-referrer-when-downgrade
server: Caddy
strict-transport-security: max-age=2592000
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 15
date: Fri, 28 Jan 2022 02:16:46 GMT

$ curl -6 --http2 -I https://demo.example.com/
HTTP/2 200
accept-ranges: bytes
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
content-type: text/html; charset=utf-8
etag: "r6edtff"
last-modified: Fri, 28 Jan 2022 02:05:39 GMT
referrer-policy: no-referrer-when-downgrade
server: Caddy
strict-transport-security: max-age=2592000
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-length: 15
date: Fri, 28 Jan 2022 02:16:46 GMT

$ docker run -t --rm --network host ymuski/curl-http3 curl -4 --http3 -I https://demo.example.com/
HTTP/3 200
server: Caddy
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
x-content-type-options: nosniff
last-modified: Fri, 28 Jan 2022 02:05:39 GMT
content-type: text/html; charset=utf-8
accept-ranges: bytes
content-length: 15
referrer-policy: no-referrer-when-downgrade
strict-transport-security: max-age=2592000
x-frame-options: SAMEORIGIN
etag: "r6edtff"

$ docker run -t --rm --network host ymuski/curl-http3 curl -6 --http3 -I https://demo.example.com/
HTTP/3 200
x-frame-options: SAMEORIGIN
etag: "r6edtff"
accept-ranges: bytes
content-length: 15
server: Caddy
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
referrer-policy: no-referrer-when-downgrade
strict-transport-security: max-age=2592000
x-content-type-options: nosniff
content-type: text/html; charset=utf-8
last-modified: Fri, 28 Jan 2022 02:05:39 GMT
</code></pre>

<p>Finally, we use <a rel="nofollow" href="https://www.ssllabs.com/ssltest/index.html">SSL Server Test</a> to verify that TLS certificates and crypto are configured securely, and use <a rel="nofollow" href="https://securityheaders.com/">Security Headers</a> to check that <a rel="nofollow" href="https://scotthelme.co.uk/hsts-the-missing-link-in-tls/">HTTP Strict Transport Security</a> is setup correctly.</p>

<h2>Conclusion</h2>

<p>This article explains how to deploy a website in a VM instance on Oracle Cloud Free Tier.<br />
It involves the following steps:</p>

<ol>
<li>Create an always free compute VM instance.</li>
<li>Enable IPv6 in the Virtual Cloud Network, subnet, and VM instance.</li>
<li>Add ingress and egress rules in the network Security List.</li>
<li>Install Caddy HTTP server and PHP.</li>
<li>Configure local iptables firewall.</li>
<li>Test the website installation.</li>
</ol>
]]>
        </description>
    </item>
    <item>
        <title>yoursunny.com is Served by Caddy</title>
        <link>https://staging.lowendspirit.com/index.php?p=/discussion/2414/yoursunny-com-is-served-by-caddy</link>
        <pubDate>Wed, 13 Jan 2021 05:21:21 +0000</pubDate>
        <category>Technical</category>
        <dc:creator>yoursunny</dc:creator>
        <guid isPermaLink="false">2414@/index.php?p=/discussions</guid>
        <description><![CDATA[<blockquote><div>
  <p>This is a personal story on how I'm keeping a 15-year-old website alive.<br />
  Read along if you want to see a comparison among the various HTTP servers and TLS certificate solutions I used during the past 10 years.<br />
  This post is originally published on yoursunny.com blog <a href="https://yoursunny.com/t/2021/yoursunny-com-caddy/" rel="nofollow">https://yoursunny.com/t/2021/yoursunny-com-caddy/</a></p>
</div></blockquote>

<p>The last rebuild of <strong>yoursunny.com</strong> was in Spring 2017, when I <a rel="nofollow" href="https://yoursunny.com/t/2017/yoursunny-com-git/">moved the whole website into git repositories</a>.<br />
It's been more than 3 years, and I think I should share an update on a few changes in the stack that serves this website.</p>

<h2>History of HTTP Servers Behind yoursunny.com</h2>

<p>Since 2011, my HTTP server of choice was <a rel="nofollow" href="https://www.lighttpd.net/">lighttpd</a>.<br />
Then, I have PHP running in FastCGI mode to serve the dynamic pages.<br />
It works, but I don't really like the lighttpd's script-like configuration structure.<br />
Moreover, there were suspected memory leaks in my setup, so that I had to use a cron job to restart the HTTP server weekly.</p>

<p>I keep hearing good words about <a rel="nofollow" href="https://nginx.org/en/docs/">nginx</a>, as well as the benefits of running PHP in <a rel="nofollow" href="https://www.php.net/manual/en/install.fpm.php">FPM mode</a>.<br />
In 2013, I <a rel="nofollow" href="https://twitter.com/yoursunny/status/409242213559070720">made the switch</a> to nginx and PHP-FPM.<br />
The declarative configuration of nginx is easy to understand and makes sense to me.</p>

<p>HTTPS came to yoursunny.com <a rel="nofollow" href="https://twitter.com/yoursunny/status/682632871740354560">at the end of 2015</a>.<br />
Like many other websites, the TLS certificates were issued by <a rel="nofollow" href="https://letsencrypt.org/">Let's Encrypt</a>, and requested through <a rel="nofollow" href="https://certbot.eff.org/">certbot</a> command line client.<br />
One problem is that, my VPS at the time had only 64MB memory, and certbot would not work in such a small amount of memory.<br />
I had to request the TLS certificate on my laptop, and then upload it to the VPS.</p>

<p>I bought a larger VPS with 1GB memory in 2016.<br />
With ample memory, certbot was able automatically obtain the certificate every other month.<br />
However, I soon found that although certbot wrote the new certificates to the filesystem, nginx would continue to use the old certificates until it is restarted.<br />
Thus, every time I receive a <em>Certificate Transparency Notification</em> email, I had to SSH into the server and restart nginx.<br />
I'm sure there are ways to automate this, but I never tried to figure out.</p>

<p>In November 2020, I had a brief interaction with <a rel="nofollow" href="https://acme.sh">acme.sh</a>, an ACME protocol client written in Unix Shell language.<br />
It's easy to understand, consumes minimal resources, and can be configured to restart nginx very easily.<br />
acme.sh quickly replaced certbot in all my setups, until I found an easier way.</p>

<h2>Caddy HTTPS Server</h2>

<p>nginx is fast, and acme.sh handles all my certificate needs.<br />
I followed the advice on <a rel="nofollow" href="https://ssl-config.mozilla.org/">Mozilla SSL Configuration Generator</a> and received an "A" rating on <a rel="nofollow" href="https://www.ssllabs.com/ssltest/index.html">SSL Server Test</a>.<br />
Things have been working well so far.</p>

<p>A few years later, I noticed that my green "A" rating turned into a yellow "B" rating, with a warning message:</p>

<blockquote><div>
  <p>This server supports TLS 1.0 and TLS 1.1. Grade capped to B.</p>
</div></blockquote>

<p>What happened?</p>

<p>It turns out that, HTTPS is more than a padlock: as the Internet evolves, the security recommendations are also evolving.<br />
An nginx configuration written in 2016, adapted to the browsers and mobile devices at that time, is no longer secure in 2020, because over the years, protocol vulnerabilities are discovered and attacks are becoming more sophisticated.<br />
If I want to keep the "A" rating, I have to keep the nginx configuration up to date with the current security recommendations.</p>

<p>Then I discovered an easier method: there's a new HTTP server called <strong>Caddy</strong>.<br />
<a rel="nofollow" href="https://caddyserver.com/">Caddy</a> is a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go.<br />
Caddy automatically obtains and renews TLS certificates, uses a hardened TLS stack powered by the Go standard library, and has a secure-by-default TLS configuration complete with OCSP stapling.</p>

<p>Instead of:</p>

<ul>
<li>nginx</li>
<li>acme.sh or certbot</li>
<li>periodically restart nginx</li>
<li>annual nginx configuration check-up</li>
</ul>

<p>I just need:</p>

<ul>
<li>Caddy 2</li>
</ul>

<h2>Caddy + nginx + PHP-FPM</h2>

<p>If I want to run a simple HTTPS server serving static files, I just need:</p>

<ul>
<li>Caddy 2</li>
</ul>

<p>In reality, yoursunny.com is a 15-year-old website with hundreds of pages.<br />
Apart from HTTPS, I also need:</p>

<ul>
<li>PHP script to render Markdown into HTML</li>
<li>PHP script to retrieve my Twitter updates</li>
<li>PHP script to display a list of blog articles on the frontpage</li>
<li>redirect rules to keep the query strings working in <code>/study</code> subsite</li>
<li>redirect rules to keep old links under <code>/work/</code> directory working</li>
</ul>

<p>All these have been written in the nginx configuration.<br />
Caddy is powerful and robust, but I'm not ready to rewrite all these into a Caddyfile.<br />
Moreover, a critical feature is missing in Caddy: there isn't a <a rel="nofollow" href="https://github.com/caddyserver/cache-handler/issues/1">caching layer</a>.</p>

<p>How I designed my PHP scripts is that, they depend on the HTTP server to support caching.<br />
For example, I have a <a rel="nofollow" href="https://bitbucket.org/yoursunny/yoursunny-website/src/670e313feccc25cc3f54d39cd4ed43bb9a5df7be/www/m/feed.php">PHP script</a> that retrieves my <a rel="nofollow" href="https://twitter.com/yoursunny">recent tweets</a> and then render them into an HTML snippet.<br />
Every execution of this script involves a network request and a Twig template execution, which would consume quite some server resources.<br />
If I was working with a shared hosting account, the script would have a temporary file mechanism such that it only performs the expensive operations every few minutes.<br />
However, since I have been able to control the HTTP server, I shifted the caching responsibility to nginx, so that the PHP script itself would be simpler.</p>

<p>To solve these problems, I decided to keep using nginx for the website logic.<br />
Therefore, the current setup is:</p>

<ul>
<li><p>Caddy:</p>

<ul>
<li>Redirect plain HTTP (port 80) to HTTPS (port 443).</li>
<li>Terminate TLS.</li>
<li>Accept HTTP/1 and HTTP/2 requests on port 443.</li>
<li>Forward all requests to nginx over a Unix socket.</li>
<li>HTTP/3 is disabled at the moment, because my current hosting provider has a DDoS protection firewall that rate-limits IPv4 UDP traffic.</li>
</ul></li>
<li><p>nginx:</p>

<ul>
<li>Process all HTTP/1.1 requests.</li>
<li>Handle redirect rules.</li>
<li>Forward PHP requests to PHP-FPM over another Unix socket.</li>
<li>Cache FastCGI responses from PHP scripts.</li>
<li>Static files are handled by nginx rather than Caddy, because some of my redirect rules are written as 404 handlers.</li>
</ul></li>
<li><p>PHP-FPM:</p>

<ul>
<li>Execute all my scripts.</li>
</ul></li>
</ul>

<h2>Security Headers</h2>

<p>Having Caddy handle TLS termination, yoursunny.com is back to the green "A" rating on <a rel="nofollow" href="https://www.ssllabs.com/ssltest/index.html">SSL Server Test</a>.<br />
Then I found a new website security rating system: <a rel="nofollow" href="https://securityheaders.com/">Security Headers</a>.<br />
It verifies and makes recommendations on a few security-related HTTP response headers from my server.</p>

<p>Starting from a big red "F", I have been reading the guidelines and making changes to the Caddyfile as I begin to understand what they are about.<br />
As of this writing, I'm receiving a yellow "B" rating on <a rel="nofollow" href="https://securityheaders.com/">Security Headers</a>:</p>

<ul>
<li>✔️ Referrer-Policy</li>
<li>✔️ Strict-Transport-Security</li>
<li>✔️ X-Content-Type-Options</li>
<li>✔️ X-Frame-Options</li>
<li>❌ Content-Security-Policy</li>
<li>❌ Permissions-Policy</li>
</ul>

<p>Not bad, if you consider that Google has a "D" and Cloudflare has a "C".</p>

<h2>Conclusion</h2>

<p>This article goes behind the scenes of my website <strong>yoursunny.com</strong>.<br />
It describes my evolving choice of HTTP servers and TLS solutions, and the current setup that combines Caddy and nginx.<br />
Caddy and nginx configuration files behind this website, along with all other source code, are published in the <a rel="nofollow" href="https://bitbucket.org/yoursunny/yoursunny-website">yoursunny-website git repository</a>.</p>

<h2>Appendix</h2>

<p>List of providers I used in recent years:</p>

<ul>
<li>LoliHosting - deadpooled</li>
<li>VPSCheap - too much downtime</li>
<li>W00tH0sting - deadpooled</li>
<li>SecureDragon - used to be nice</li>
<li><a href="https://staging.lowendspirit.com/index.php?p=/profile/SMARTHOST" rel="nofollow">@SMARTHOST</a> - very stable and fast</li>
<li>SpartanHost - good speeds to China, but DDoS protection rate-limits UDP so I can't have HTTP/3 and DDoS protection at the same time</li>
</ul>
]]>
        </description>
    </item>
   </channel>
</rss>
