$ BLR//CODE

Home / Work

In-house build · In-house (BLR Code)

A hardened public VPS with a push-based deploy pipeline

The site you're reading: an ARM cloud VM locked to three ports, deployed from a CI runner that the server cannot reach back to, with scanning as a release gate.

Ubuntu 24.04 (ARM64)DockerTraefikGitea ActionsTrivy

Context

This site needed somewhere to live. Rather than a shared host, it runs on a small ARM cloud VM — which made it a useful exercise in doing the boring parts properly, since it’s publicly reachable and it’s ours to get wrong.

Hardening

Standard measures, applied deliberately rather than by running a script off the internet:

  • Key-only SSH, root login disabled, and an explicit allow-list of exactly which account may log in at all.
  • fail2ban on the SSH service.
  • Firewall closed to everything but 22, 80 and 443 — and, importantly, configured at both the host and the cloud provider’s virtual network layer, because those are two independent gates and only one of them is visible from inside the machine. The details of why that catches people are in Your cloud firewall rules probably don’t do what you think.
  • Unattended security upgrades enabled, so OS patches land without waiting for someone to remember.
  • Unused inherited services — rpcbind among them — disabled rather than left listening.

One deliberate non-action: we did not install ufw. The provider’s image ships pre-seeded iptables rules, and layering a second firewall tool on top of those produces a policy that is neither tool’s intent.

The deploy pipeline, and why it pushes

The interesting constraint: our Git server runs inside a private network segment that the public VPS cannot reach. The conventional pull-based deploy — server fetches from repository — is therefore impossible, and making it possible would mean exposing the Git server to the internet, which is precisely the thing the segmentation exists to prevent.

So the pipeline pushes instead. On every commit to main, a CI runner inside the private network:

  1. Checks out the repository
  2. Scans the container base images with Trivy, failing the run on any CRITICAL finding that has a fix available
  3. Ships the working tree to the VPS over SSH using a deploy key held as a repository secret
  4. Runs the container build and restart remotely
  5. Sends a notification only if something failed

The direction of that connection is the whole point: the private network initiates, the public server accepts. Nothing inbound is required, and compromising the public VPS gets an attacker no path back toward anything that matters.

The scanning gate and the reasoning behind its thresholds are written up in A deploy pipeline that refuses to ship known-vulnerable images.

Serving

Traefik terminates TLS with automatic Let’s Encrypt certificates and renewal, redirects www to the apex domain and HTTP to HTTPS, and proxies to an nginx container that serves the pre-built static site. The nginx image runs unprivileged — as a non-root user, on a high port internally — because a static file server has no reason to hold root.

The site itself is compiled to plain HTML at build time. There is no database, no application runtime in the request path, and nothing to patch on the content side.

Monitoring

Two independent checks: an external uptime monitor testing both the TCP port and the HTTPS endpoint, and a metrics agent on the host reporting CPU, memory and disk with alert thresholds. Both alert to the same place, and both are silent when things are fine.

The agent’s port is restricted to a single source address at both the provider firewall and the host — a monitoring agent is still an exposed service, and it gets treated as one.

Why this is listed

It’s our own infrastructure, not a client project, and it’s marked as such. It’s here because it’s inspectable: the site you’re reading is served by it, the certificate is valid, and the architecture described above is the architecture running. That seemed more useful than a description of work you’d have to take on faith.

CTA/ — Start a conversation

Tell us what you're building.

Write a few lines about your project and you'll get a reply within one business day — including an honest view of whether we're the right fit.

Start a conversation →