12.1 Run Exposed Self-Hosted Apps at the Latest Version
#The Risk
Self-hosted apps you run off the shelf - automation tools, CI/CD, AI agent builders, admin dashboards - ship security fixes in new releases. Running an old version is like keeping last year's lock on your door after the maker publicly announced it can be picked: the instructions are out there, and bots scan the whole internet for doors still using it. A real example: a popular AI-builder tool had a critical flaw disclosed publicly and a patch shipped, yet instances that never upgraded were mass-exploited many months later - the attackers were simply working down a list of known-vulnerable versions. (Technical: an internet-reachable app on an N-day version is a sitting target; public CVE + public proof-of-concept + automated scanners means compromise is a matter of when, not if.)
The Solution
For any third-party app you EXPOSE to the internet, stay on the latest stable release and patch promptly when a security fix lands. Watch the project's releases or security advisories so you hear about a fix when it ships, not when you get breached. Important nuance: this is the OPPOSITE of how you treat code LIBRARIES (see 11.5, "pin to currently-running, not latest"). Libraries get pinned-and-aged because a brand-new release could itself be a supply-chain attack. A self-hosted APP is different: the bigger danger is a known, already-public CVE in the old version you are exposing, so for these, latest-and-patched wins. Know which of the two you are dealing with.
The Fix
# Know the version you actually run (don't guess from memory)
docker ps --format '{{.Image}}' # e.g. someapp/builder:1.2.3
# Compare to the latest stable release for that project, then upgrade.
# Back up data / volumes first and read the release notes for breaking changes.
docker compose pull && docker compose up -d
# Subscribe to advisories so a fix reaches you on release day:
# - GitHub repo -> Watch -> Custom -> Security advisories
# - or a CVE/advisory monitor scoped to the apps you actually run
# Then patch promptly. An exposed admin app is the worst place to sit on an N-day.The asymmetry IS the lesson: for LIBRARIES you bundle into your build, aging is safety (a fresh release might be poisoned). For self-hosted APPS you expose, aging is danger (the old version has a public, exploited CVE). Same word "version", opposite advice - choose based on whether the threat is a malicious NEW release or a known-bad OLD one.
12.2 Expose Only the Public Runtime, Gate the Admin Surface
#The Risk
Most self-hosted apps put two very different things on the same domain: a small PUBLIC part real users actually need (the endpoint a chatbot calls, a webhook), and a large ADMIN part - the editor, the settings, the management API, the login - that only YOU need. By default the whole thing is open to the internet. That is like running a shop where the front counter and the back office with the safe share one unlocked door on the street. Every admin and management path you leave open is attack surface: a brute-force target, and a launchpad for any future flaw in those paths.
The Solution
Decide which handful of paths the public genuinely needs, allow ONLY those at the edge, and block everything else - the admin UI, the editor, the management/REST API, the login - for anyone who is not you. You shrink the app's internet-facing surface from "everything" down to "the few runtime paths", so a bot can no longer even reach the admin login or a vulnerable management endpoint. Keep the allow-list tight and check it against real traffic: if you are not sure a path is needed by users, it probably is not.
The Fix
# At the edge (CDN / Worker / reverse proxy): default-DENY the app's host for
# outsiders, allow ONLY the few public runtime paths real users hit.
if (isThirdPartyAppHost && !isOperator(request)) {
const path = url.pathname;
const publicPath = path.startsWith('/api/v1/prediction') || path.startsWith('/webhook');
if (!publicPath) return new Response('Forbidden', { status: 403 });
}
# Everything else - editor UI, /rest, /credentials, /settings, the login page,
# the management API - is now unreachable from the public internet.
# Build the public list from your REAL request logs, not from guesses.Derive the public allow-list from actual traffic, not assumptions: pull the app's request log, see which paths real users hit, allow only those. Over-allowing re-opens the surface you are trying to close; under-allowing breaks a feature. When unsure, block and wait for a complaint - far safer than leaving an admin path open to the internet.
12.3 Put High-Value Admin UIs Behind an Identity Wall
#The Risk
Some self-hosted apps are not just "an app" - they are a control plane. If someone gets into the tool that deploys and manages all your other servers, or that holds all your secrets, the blast radius is your ENTIRE setup, not one app. For these, even the LOGIN PAGE being reachable from the internet is a risk: it is a brute-force target, and if the app ever has a flaw that skips its login (an "unauthenticated" bug), being reachable at all means game over. Gating individual paths (12.2) helps, but a crown-jewel admin tool deserves more.
The Solution
Put an identity check IN FRONT of the app, at the network edge, so a visitor must prove who they are BEFORE any request reaches the app - including the app's own login page. Think of a guard at the building entrance checking ID before you even reach the office door's lock. Now anonymous traffic cannot touch the login or any unpatched endpoint at all; only an approved, signed-in person gets through, from anywhere, with no list of allowed locations to maintain. Layer it OVER the app's own login so there are two independent gates, and set up a backup sign-in method in case your primary one is unavailable.
The Fix
# Use an edge identity gate (e.g. a Zero Trust / "Access" product from your CDN)
# in front of the app's hostname. Conceptually:
#
# visitor -> [Edge identity wall: prove who you are] -> [App's own login + 2FA] -> app
#
# - Allow only specific people (your email / your SSO); block everyone else at the edge.
# - Add a BACKUP login (e.g. a one-time email code) so a lost primary
# credential does not lock you out.
# - For machine callers (a monitor, a script), issue a SERVICE TOKEN instead of a
# human login, so automation still gets through the wall.
# Net effect: the app's login page and any future unauth bug are unreachable
# by anonymous internet traffic.This is the strongest layer, so reserve it for the high-blast-radius tools - the thing that controls your other servers, holds your secrets, or can deploy code - rather than every app; for ordinary apps the path-gating in 12.2 is usually enough. Keep it ON TOP of the app's own authentication, never instead of it: two independent gates mean a failure or bypass in one is still caught by the other.