afterguard

AfterGuard Released to Put a Lock on Old Apps That Have None

Aftermarket 0.6.0 adds AfterGuard, run as aftermarket guard. It’s for the app that was never meant to face anyone: an admin panel from 2014 with no login, an internal API with no rate limit, a debug page someone forgot to switch off.

Rewriting those apps rarely happens. The usual fix is a few lines of nginx config that nobody tests, or an IP allow list that stops working the day someone moves offices. AfterGuard stands in front of the app instead, and turns away what your rules don’t allow.

A config

version: 1
default: deny
clients:
  - id: billing
    api_key_sha256: f04bf1cd97cf76f0f2d928e4e1fa56033548cf50826db9f0c265d1f349e28c84
  - id: admin
    basic_user: admin
    basic_password_sha256: 729d88640e95cc2e6a8a188e992f87a3502602dc7ed49543d9454ea1bc0fc429
rules:
  - id: status
    when: {method: GET, path: /status}
    allow: true
  - id: no-debug
    when: {path: /debug/**}
    deny: 404
  - id: reports
    when: {method: GET, path: /reports/**}
    client: [billing]
    allow: true
    limit: {rate: 120/m, per: client}
  - id: admin-office
    when: {path: /admin/**}
    from: [192.168.0.0/16]
    client: [admin]
    allow: true
response:
  remove_headers: [Server, X-Powered-By]

The first rule that matches decides, and anything no rule allows gets 403. Keys and passwords sit in the config as SHA-256 hashes, made with aftermarket guard hash, never as themselves. The app learns who called from a header AfterGuard sets, and a client can’t set that header itself, under any spelling.

What it turns away

  • The obvious: no credential, a wrong one, the wrong network, too many requests. Five wrong passwords in a minute and that address waits, even with the right one.
  • The sneaky: requests built to be read one way by a gateway and another way by the app behind it. A path like /status/..%2fadmin, a request with both Content-Length and Transfer-Encoding, a header folded over two lines, a X-Original-Url header that some frameworks use to replace the path. AfterGuard rejects these with 400. It never cleans them up and passes them on, because a cleaned-up request is still a guess about what the app will do.
  • The floods: limits live in a table of fixed size, and IPv6 addresses count by their /64, so a client can’t fill memory or reset other clients’ limits by changing addresses.

Every decision is one JSON line in the log, without credentials, cookies or query values. --dry-run forwards everything and logs what would have been turned away, so a config can meet real traffic before it blocks anything.

What it doesn’t do

No single sign-on yet, and no attack signatures for SQL injection and the like. Those give a false sense of safety and block real traffic. The threat model lists what AfterGuard defends against and what it leaves to others. The one thing it can’t do for you is make sure the app is reachable only through AfterGuard; guard check reminds you every time.

The AfterGuard reference has the full format, and the download page has the binaries.