The Security Audit That Took a Self-Hosted Server From a Worry to an Asset
Client engagement · Details anonymised at the client’s preference
The situation
Self-hosting your own tools gives you control and cuts costs, but it hands you a responsibility most small businesses are not set up for: the server is now yours to secure. A Cyprus training provider ran its CRM, its customer data and its day-to-day operations on its own server, and like most self-hosted setups it had been stood up to work, not to withstand attack. The doors were functional but not locked: default settings, permissive access, and no active defence against the constant background noise of automated attacks that every public server receives.
What we built
We hardened the server end to end and then had it audited to prove the work, rather than take it on faith. That meant active defence against brute-force attempts, so repeated failed logins get an address blocked automatically, tighter firewall rules so the containers running the business can only reach what they genuinely need, locked-down settings on the web platform so a compromised page cannot run dangerous commands, and a stricter policy for who can log in and how. Then we ran a recognised security audit against the whole machine to score it and surface anything missed.
What changed
- The audit score speaks for itself. The hardened server came back at an A grade, 81 out of 100, well above the exposed baseline a fresh self-hosted setup starts from.
- Automated attacks get shut out. The steady stream of brute-force login attempts that every public server sees is now detected and blocked with nobody watching.
- Blast radius is contained. Tighter firewall and platform restrictions mean that even if one part is compromised, it cannot reach across and take the rest of the machine with it.
- Security is measured, not assumed. A real audit score turns “we think it is secure” into a number you can show a client or an insurer, and re-run to prove it stays that way.
Why it matters
A self-hosted server holding customer data is only a saving if it is also safe. An unhardened one is a breach waiting to happen, and under GDPR a breach is not just downtime, it is a reportable event with real consequences. Hardening is the cheap insurance that keeps the cost advantage of self-hosting from turning into a liability.
Where it stands
Live on the production server, with the audit score on record. Hardening is not a one-time job, so the same checks can be re-run to confirm the machine has not drifted, and the audit gives a clear target to hold it to over time.
Running your business on a server you manage yourself? We harden it and give you an audit score you can stand behind. Book a free audit and we will tell you where you stand today.
How it works
The CRM runs as a single Docker Compose stack on one internal bridge network, where only the reverse proxy publishes ports 80 and 443 to the outside world; the database, cache and object store never leave the internal network. The application is a Node and TypeScript API with a background worker built from the same image, a separate web front end, a PostgreSQL 17 database with Row-Level Security, a Redis-backed job queue, and S3-compatible object storage. Every state-changing operation emits a domain event onto an internal event bus with two consumers: one writes an append-only audit record that database triggers make immutable, and one matches automation rules and enqueues background jobs. Outbound calls to mail servers, third-party connectors and language-model providers all pass through a single egress guard before any request leaves the host.
- Reverse proxy (auto-HTTPS): the only component published to the network, terminating TLS on 80 and 443 and writing the access log the threat-detection engine reads.
- API and worker: one codebase, the worker started from the same image in a worker role, so business logic and background jobs stay in sync.
- Web front end: server-rendered application UI, including the client review portal.
- PostgreSQL 17 with Row-Level Security and an append-only, trigger-protected audit log.
- Redis-backed queue for scheduled jobs, automations and rate-limit and lockout state.
- S3-compatible object storage (self-hosted, internal-only) for uploads and backups.
- Internal event bus fanning every domain event out to the audit and automation handlers.
Built with
- TypeScript and Node across the API, worker and front end.
- A NestJS API and worker, with a Next.js web front end.
- PostgreSQL 17 with Prisma, Row-Level Security enforced by a non-superuser database role.
- Redis with a background job queue for schedules, automations and throttling.
- S3-compatible object storage, self-hosted and internal-only.
- Docker Compose on a single internal bridge network, fronted by an auto-HTTPS reverse proxy.
- An nftables firewall with an open-source HTTP threat-detection engine reading the proxy access log.
- CI security gates: dependency audit, static analysis (SAST) and secret scanning on every push.
Security and compliance
- Brute-force defence: per-account login lockout that fails closed, backed by Redis, plus an open-source threat-detection engine that parses the reverse-proxy access log against 19 HTTP attack scenarios and drops offending IPs at the nftables firewall.
- Tighter firewall and least-privilege containers: only the reverse proxy publishes 80 and 443, while the database, cache and object store stay internal-only; application containers run as non-root with all Linux capabilities dropped and no-new-privileges set.
- SSRF and egress restriction: a shared fetch guard blocks requests to private RFC1918 ranges, 127.0.0.1 and the 169.254.169.254 cloud-metadata address, requires HTTPS, and re-validates redirects and DNS-rebinding on every outbound call, covering mail-server tests, connectors and language-model providers.
- Platform hardening: real PostgreSQL Row-Level Security enforced by running the app under a non-superuser role; an append-only audit log kept immutable by database triggers that block updates and deletes; AES-256-GCM encryption of connector secrets; security response headers; and host-only authentication cookies.
- Stricter login policy: the JWT signing algorithm pinned on every verify, refresh-token revocation via a token-version counter, and bcrypt password hashing.
- Data-egress control for AI features: a PII sanitizer wraps all language-model flows including tool-call payloads, and agents run under an allowlist and a service-account permission ceiling with human approval required on sensitive actions.
- Supply chain: an osv-scanner v2.4.0 run over 1176 packages found nothing production-exploitable, and dependency audit, SAST and secret scanning run in CI on every push.
- Audit result: a from-scratch re-verification, run as parallel review sweeps against the live system, closed with 0 Critical and 0 High findings open, and a dynamic API scan exercised 255 API operations with 0 exploitable findings; the recognised audit grade is (measuring).
- GDPR data lifecycle: right-to-erasure with audit pseudonymisation, data export, a daily retention purge, PII redaction in error logs, and daily off-site encrypted backups.
Want this working in your business?
Book a free 30 minute discovery call. We map your workflows and tell you honestly what is worth automating. No commitment, no sales pressure.
Book a discovery call