DEV Community

AlektoReef
AlektoReef

Posted on

Four security decisions that look like nothing and are not

Disclosure: these are decisions from Tessera, which I work on. They are all small enough to copy into your own service, which is why they are worth writing up.

Security feature lists are made of nouns: encryption, RBAC, SSO, audit. The things that actually decide whether a system holds up are smaller than that and never make the list. Here are four of ours, with the reasoning, including the case where we made the trade in the direction most people do not.

1. The login rate limit ignores X-Forwarded-For

Rate limiting a login endpoint is table stakes. The question is what you count against.

The natural implementation reads X-Forwarded-For, because your service is behind a load balancer and the real client address is in that header. Almost every tutorial does it this way.

The problem: X-Forwarded-For is a request header. If your service trusts it, an attacker sets a different value on every request and each one gets its own bucket. You have not built a rate limit, you have built a counter that resets on demand, and the dashboards will look perfectly healthy while the endpoint is being brute-forced.

We count against the real TCP connection address instead. That is the peer address of the socket, which an attacker cannot forge without actually controlling that address.

The cost is real and worth naming. Behind a reverse proxy, every request arrives from the proxy's address, so the limit applies to the proxy as a whole rather than per client. That is a worse experience in some topologies, and people will file bugs about it.

We took that trade because a rate limit that can be bypassed by setting a header is not a degraded rate limit, it is the absence of one — and the absence is worse than useless, because it looks like presence. If you do want per-client limits behind a proxy, the answer is an explicit allowlist of trusted proxy addresses whose forwarded headers you accept, not blanket trust of a header.

2. Target addresses are validated to prevent SSRF

Our controller is, by design, a machine with network access to everything interesting in the estate. It also has an admin UI where you register a target by typing a hostname.

Put those two facts next to each other and you have an internal network scanner with a web interface. Register 169.254.169.254 as a target and the controller will happily dial the cloud metadata endpoint for you. Register a loopback address and it will connect to itself.

So target hosts are validated on creation and on update — the second one matters, because validate-on-create with an unchecked update path is a very common way to have this bug while believing you fixed it. Loopback, link-local and the other address ranges that turn the service into a scanner are rejected.

The general shape: any service that takes a user-supplied address and connects to it needs this check. That includes webhook configuration, avatar-by-URL, PDF renderers that fetch images, and anything with an "import from URL" field. It is the same bug every time.

3. Host keys are pinned on first use

SSH's host-key model is the reason StrictHostKeyChecking exists and the reason most people have typed yes without reading the fingerprint. When a broker connects on behalf of a hundred engineers, "did anyone check the fingerprint" needs a better answer than "probably not".

We store the target's host public key on first connection and reject changed keys afterwards. This is trust-on-first-use, raised from an individual's known_hosts to an organisational record.

What it catches: a silent man-in-the-middle substitution after the fact. If someone puts themselves between the controller and a target, the key changes and the connection fails loudly instead of succeeding quietly.

What it does not catch, and I want to be plain about this: the first connection. TOFU is trust on first use, and if the attacker is already in position when you register the target, you pin their key. The only real fix is out-of-band host-key distribution, which almost nobody runs. Anyone claiming TOFU closes this hole is overselling it.

4. Stored credentials cannot be read back

The console is write-only for secrets. You can store a credential and you can replace it. There is no path — UI or API — that returns it.

This is more annoying than it sounds. Users want to check what they typed. Support wants to verify a password before debugging a connection failure. Every so often someone asks for a "reveal" button and there is always a plausible reason.

The reason to refuse: a read path is a read path. Once it exists, it is reachable by whoever holds an admin session, whoever finds an IDOR in that endpoint, and whoever ends up with a token that was scoped more broadly than intended. The compromise of one admin account stops being "an attacker can grant themselves access, which is logged" and becomes "an attacker walks away with every production credential you have, silently".

The mitigation for the annoyance is a connectivity test that uses the credential without returning it. That covers the legitimate use case, which was never really "show me the password" — it was "tell me whether this works".

The pattern

All four decisions have the same shape. There is a version that is easier to build and nicer to use, and a version that holds when someone is actively working against it. The gap between them is invisible in normal operation, which is exactly why it survives code review, and exactly why it should be written down somewhere rather than living in one engineer's head.

None of these are clever. Every one of them is the boring choice. That is the point.


More on Tessera's security model, including the hardening checklist and the parts our own docs describe as best-effort rather than guaranteed: https://tessera.company/docs/operations/secure/

Top comments (0)