Winston Cronenwett

How this site runs

This site is served from a server I provisioned, hardened, and maintain myself. Here is how it is put together, and what I learned putting it together.

The basics

Why not use a website builder?

Because a résumé can say I administer Linux servers, but a running server says it better. A hosted website builder would have produced a nicer-looking page in an afternoon and shown nothing about how I work. Building it this way means every layer, from DNS to the firewall to the web server, is something I chose, configured, and can explain.

It also keeps me honest. If the certificate lapses or a patch breaks something, there's no support team to call. That's the job I'm applying for, in miniature.

Where is it hosted?

On a single virtual private server at Akamai Cloud (formerly Linode). It's their smallest plan, one shared CPU and 1 GB of memory, which is plenty for a static site. Nothing about the hosting is managed: Akamai provides the virtual machine and a network firewall in front of it, and everything from the operating system up is mine to run.

Why Rocky Linux?

Rocky Linux is a free, community-built rebuild of Red Hat Enterprise Linux. I chose it because it behaves like the servers large organizations actually run: the same package manager (dnf), the same firewall (firewalld), and SELinux turned on by default. Practicing on something close to production is more useful than practicing on whatever is easiest.

The server runs Rocky Linux 10. Akamai's image is deliberately minimal, which meant installing a few things I'd normally take for granted, including the firewall itself.

Security

How is the server secured?

In layers, so that no single mistake leaves it exposed:

  • SSH accepts keys only. Password and keyboard-interactive login are off, root cannot log in directly, and only one named administrative account is allowed in. Those settings live in their own drop-in file rather than in the main SSH config, so a package update can't silently overwrite them.
  • Two firewalls. Akamai's cloud firewall filters traffic before it ever reaches the server, allowing only SSH, HTTP, and HTTPS. On the server itself, firewalld enforces the same rules, with unneeded services such as the Cockpit admin console removed. If either layer is misconfigured, the other still holds.
  • SELinux is enforcing. Every file and process carries a security label, and the kernel refuses access that the policy doesn't explicitly allow. Even if the web server were compromised, it could only read files labeled as web content. Plenty of tutorials tell you to turn SELinux off when it gets in the way; I'd rather learn to work with it.
  • Automatic security patching through dnf-automatic, so security fixes land without waiting for me to log in.

You'll notice I'm describing the design rather than publishing account names or exact configuration. The design is worth showing; the details are only useful to someone trying to get in.

How does HTTPS work?

The web server is Caddy, which requests certificates from Let's Encrypt automatically and renews them before they expire. There's no cron job to forget about and no certificate files to copy around. The same configuration serves both the bare domain and the www address, and redirects plain HTTP to HTTPS.

For that to work, Let's Encrypt has to be able to reach this server directly to confirm I control the domain. That's why the DNS records point straight at the server instead of routing through Cloudflare's proxy. More on that below.

What's going on with DNS and email?

The domain is registered with Cloudflare, which also hosts its DNS. The site is reachable over both IPv4 and IPv6, with the www name pointing at the main domain.

DNSSEC is turned on, which cryptographically signs the DNS records so a resolver can tell if someone has tampered with the answer on its way to you.

This domain doesn't send email, and I've said so publicly. Its SPF record authorizes no servers at all, and its DMARC policy tells receiving mail servers to reject anything claiming to come from it. That makes it much harder for someone to send phishing mail that appears to come from my name. It's a small thing that a lot of domains skip.

Deploying and running it

How do changes get deployed?

The site lives in a public Git repository on GitHub. The server keeps its own clone outside the web root, and a short deploy script does three things:

  • git pull --ff-only fetches the latest version, and refuses to proceed if the server's copy has somehow drifted from GitHub rather than quietly merging.
  • rsync --delete mirrors the repository into the web root, so a file removed from the repository is removed from the site too. The .git directory is excluded so the repository's history is never served to the web.
  • restorecon resets the SELinux labels on everything that was copied, so the web server is allowed to read it.

That last step is there because I learned the hard way what happens without it.

Why is the site so plain?

On purpose. It's three HTML pages and one stylesheet. There's no JavaScript, no framework, no web fonts, no analytics, no cookies, and no requests to any server but this one. It loads almost instantly, it works on any device, it adapts to your system's light or dark setting, and there's nothing in it that needs patching. The interesting part of this site is underneath it.

What went wrong along the way?

A few things, each of which taught me something:

  • SELinux silently blocked files I'd copied into place. Files copied into a directory can keep the wrong security label, and the service trying to read them just fails, often without an obvious error. It happened with my SSH keys and again with the website files. The fix is restorecon, and now it runs on every deploy.
  • Cloudflare's proxy broke two things at once. With the proxy on, certificate issuance failed and I couldn't reach the server over SSH by its domain name, because the name now pointed at Cloudflare instead of at me. Turning the proxy off for these records fixed both. If I turn it back on later, I'll add a separate unproxied hostname for administration.
  • The minimal image was more minimal than expected. Some commands I assumed would be there, including the firewall, weren't installed. dnf provides finds which package supplies a missing command.
  • Docker and firewalld don't get along. Docker writes its own firewall rules that can bypass firewalld entirely, so a container port can end up exposed even when the firewall looks locked down. When I add containers to this server, I'll use Podman, which is native to Red Hat-family systems and doesn't need a privileged background daemon.

What happens if something breaks?

For a content problem, I can swap in a maintenance page in seconds. For anything worse, stopping the web server takes the site offline immediately while leaving the server reachable for me to fix it.

The honest gap right now is recovery. The site's content is safe in Git, so it can always be redeployed, but the server's configuration exists only on the server and in my notes. Closing that gap is the first item on the list below.

What's next?

  • An Ansible playbook that builds this entire server from a blank machine: packages, SSH hardening, firewall, SELinux, Caddy, and the deploy setup. The goal is to be able to destroy the server and have an identical one running in minutes.
  • Push-to-deploy with GitHub Actions, so a change merged on GitHub goes live without me logging in.
  • A live status page reading from self-hosted monitoring, so you can see the server's uptime for yourself.
  • Off-server backups and external alerting, so I hear about an outage before anyone else does.
  • A longer build log walking through each of these steps in more detail.

Can I see the source?

Yes. Everything on this site is on my GitHub. As the automation above comes together, it'll land there too.