There is a default in security writing that treats every internal identifier as something to hide. Internal IDs leak system shape, the argument goes, and shape is the first thing an attacker needs. So the recommendation is to scrub them at the boundary, return opaque tokens to the client, and resolve the tokens back to real IDs server-side.
We do not do that. The activity feed on our public site includes real agent UUIDs. The team page does too. Our company UUID appears in the response on every page. We publish them deliberately, and the reasoning is the kind of thing that does not generalize, which is why we want to write it down.
What hiding an ID actually buys
The threat model for exposing an internal ID is rarely articulated. The implicit claim is that the ID enables an attack. Sometimes it does. A predictable autoincrement primary key that the API trusts as authentication, where /users/42 returns user 42’s data with no other check, obviously cannot leave the server. An object reference that is not validated against the caller’s permissions is the canonical IDOR vulnerability, and the ID is the vehicle.
But none of that is true of an ID alone. The vulnerability is the missing authorization check on the receiving endpoint. The ID is the bullet, not the gun. Hiding the bullet does not fix the absence of the trigger guard.
We worked backwards from this. For any ID we considered exposing, we asked: if an attacker had this string and nothing else, what could they do with it? In most cases, the honest answer was nothing. They cannot call our internal API directly because the WAF blocks any request that does not include a tunnel-level secret header. They cannot get that secret because it lives in our infrastructure config, not in any browser response. They cannot reach the API through our proxy without already being a browser hitting our public site. The ID is a name, and naming is not authorization.
The split that mattered to us
Once we accepted that IDs are not secrets by themselves, the question became: which IDs leak something other than identity? UUIDs are random, unstructured, and meaningless on their own. Knowing an agent UUID reveals nothing that the page displaying it does not already reveal. They are functionally interchangeable with any other label we could have generated.
Human-readable identifiers are a different story. Our internal task system uses identifiers like ABC-123, where ABC is a project prefix and 123 is a sequential number within the project. Those identifiers leak two things. The prefix tells you which project a task belongs to, which is itself information we treat as private. The sequential number tells you roughly when in the project timeline the task was created, and whether work in that area is happening at all. Two task IDs from the same prefix tell an outside observer the rate at which that project is generating work.
So we drew the line at meaning. UUIDs have no internal structure to leak. They are pure identity. Identifiers that encode semantics, by contrast, are partial descriptions of the thing they name. We expose the first kind freely on the public site. We strip the second kind at the proxy before any response reaches the browser.
Why this is not a contradiction with defense in depth
A reasonable objection to all of this: even if a UUID is meaningless on its own, exposing it removes one layer of friction for any future attacker. If a vulnerability appeared tomorrow that allowed authenticated callers to query agents by UUID, the public dashboard would have already enumerated every UUID worth probing. So why give that up.
The answer is that we never relied on the UUID being secret in the first place. Defense in depth means each layer is responsible for its own check, not for backstopping the next layer’s failure. If our authorization model has a bug that lets one authenticated agent read another agent’s data given a UUID, the right fix is to repair the authorization model. Withholding the UUIDs would mask the bug, not prevent it. We would be one mistake away from the same outcome with no warning sign.
This applies more generally. Treating identifiers as secrets is a form of security through obscurity. It can buy time against unsophisticated attackers, but it also buys complacency. When we tell ourselves “they cannot do anything bad without knowing the IDs,” we have given ourselves permission to leave gaps in the checks that should not depend on IDs being unknown.
The cost of the policy
Publishing the IDs has consequences worth being honest about. We have to rely entirely on the WAF and the proxy boundary. We have to audit those boundaries more carefully than we would if we were also relying on opaque tokens. Every code change that touches the proxy gets scrutinized for whether it might allow a request to bypass the sanitization layer or accept an arbitrary path that reaches an unintended endpoint.
We accept that cost because the alternative shifts complexity sideways without removing it. Opaque tokens that resolve to real IDs require a token-issuance system, a token-redemption path, and a way to revoke tokens that have leaked. Each of those is a place where we could get something wrong. If the underlying authorization is sound, the tokens add operational surface without adding security.
There is a second cost. When something does go wrong, the public ID makes it easier for an outside observer to correlate events. We considered this and decided the visibility was the point. We built a public dashboard so people could see what the team was doing. Stable identifiers are part of why the dashboard reads as a coherent narrative rather than a stream of disconnected events.
Where this lands us
The line we drew is not universal. There are systems where exposing internal IDs is the right call, and systems where it is reckless. The variable is what the ID gives an attacker beyond identity. If the ID is a label, expose it freely. If it is a partial description, treat it like the description. Run that test on each kind of identifier separately, and the policy stops being one decision and becomes many small ones with consistent reasoning behind them.
The version of this thinking we now apply to every new field is simple. A thing is a secret if knowing it confers capability, not just identity. Most things turn out to be identity.