NoctisNode
← 가이드Security2026-09-029 min

Securing self-hosted services beyond the server

Once the host is hardened, the app becomes the attack surface — least privilege, secret management, database exposure, backups and the specific settings that matter per service.

A hardened server running an insecure service is still insecure. Once the host itself is locked down — see the hardening checklist — the remaining risk is in the applications. This guide covers the practices that separate a properly secured self-hosted stack from one that merely looks tidy.

Never expose the app directly

The single highest-value rule: your application binds to 127.0.0.1 and only the reverse proxy faces the internet. Caddy or Nginx terminates TLS, handles certificates, and proxies to the app — the app's port never opens to the world.

Least privilege for everything

  • ▹Run each service as its own unprivileged user, not root and not shared.
  • ▹Use a separate database user per application with only the permissions it needs.
  • ▹Give the app write access only to its own data directory, not to /etc or your home.
  • ▹Do not run multiple apps as the same user — one compromise then reaches all of them.

Secrets management

API keys, database passwords and tokens do not belong in a git repo, in a shell history file, or in the app's source. Keep them in a file with 600 permissions owned by the service user, or in a systemd EnvironmentFile that is not world-readable. Our SSH guide covers the key hygiene that starts this discipline.

Note · Rotate any secret that has ever been in a repo, a screenshot, or a paste — assume it is compromised once it has been exposed.

Databases must never be internet-facing

A database on 0.0.0.0 is the classic self-hosting mistake. Bind Postgres or MySQL to 127.0.0.1, require a password, and reach it over the local socket or loopback. If a remote client needs access, tunnel it — do not open 5432.

Service-specific settings that matter

ServiceSetting that actually mattersWhy
SSHPasswordAuthentication noRemoves brute force entirely
Caddy/NginxApp bound to 127.0.0.1Only the proxy is exposed
Postgreslisten_addresses = localhostDatabase not reachable externally
DockerDo not publish DB ports; use internal networksDefault publishing leaks the DB
WireGuardKeys-only, UDP port from firewallNo password surface at all
MailNo open relay; SPF/DKIM/DMARCOtherwise you become a spam source

Backups are a security control

Ransomware and accidental deletion both work by destroying what you would restore from. Encrypted, off-site, tested backups are what make a security incident survivable. Our Restic guide covers encrypted off-site backups, and a backup you have never restored from is not a backup.

Keep it patched

Unpatched app dependencies are the most common real-world entry point, ahead of the host itself. Enable automatic security updates, and make updating a dependency a routine part of deploying rather than an emergency.

FAQ

QIs exposing an app on 80 without a proxy fine?

AIt works, but you then handle TLS, headers and rate limiting yourself, and the app port is what the internet sees. A reverse proxy is the better default.

QHow do I keep secrets out of git?

AKeep them in a separate file with 600 permissions owned by the service user, referenced by a systemd EnvironmentFile, and never committed.

QHow often should I test backups?

AQuarterly at minimum — restore a file and a database from the backup. An untested backup is an assumption, not a plan.

QWhat is the most common self-hosting mistake?

APublishing a database port. Bind it to localhost and access it over the local socket.

Securing self-hosted services beyond the server · NoctisNode