[{"data":1,"prerenderedAt":938},["ShallowReactive",2],{"page-en-\u002Fgarden\u002Facting-on-a-humans-behalf":3,"backlinks-en-\u002Fgarden\u002Facting-on-a-humans-behalf":380},{"doc":4,"isFallback":367},{"id":5,"title":6,"body":7,"description":366,"draft":367,"extension":368,"meta":369,"navigation":370,"notice":371,"path":372,"seo":373,"stage":374,"stem":13,"tags":375,"topic":378,"__hash__":379},"garden_en\u002Facting-on-a-humans-behalf.md","Acting on a Human's Behalf",{"type":8,"value":9,"toc":355},"minimark",[10,14,23,26,45,48,53,64,67,94,107,110,114,117,124,135,143,147,154,164,170,174,177,199,210,213,217,220,231,241,255,263,266,276,296,299,303,306,324,335,338,342,348],[11,12,6],"h1",{"id":13},"acting-on-a-humans-behalf",[15,16,17,18,22],"p",{},"Put a service between a person and a backend and you've created an identity question, whether or not you notice. The person authenticated to ",[19,20,21],"em",{},"your service",". Your service now has to say something to the backend. What, exactly?",[15,24,25],{},"There are two credentials in play here, and the entire design rests on keeping them apart:",[27,28,29,38],"ol",{},[30,31,32,33,37],"li",{},"The one the ",[34,35,36],"strong",{},"caller"," presented to your service.",[30,39,40,41,44],{},"The one your ",[34,42,43],{},"service"," presents to the backend.",[15,46,47],{},"The tempting move — and the one worth naming as a mistake up front — is to make the second a copy of the first.",[49,50,52],"h2",{"id":51},"why-passing-the-token-through-breaks-things","Why passing the token through breaks things",[15,54,55,56,59,60,63],{},"If a service simply forwards the caller's token to the backend, the backend can no longer tell whether a request arrived ",[19,57,58],{},"through the service"," or ",[19,61,62],{},"directly",". Both look identical on the wire, because they are identical.",[15,65,66],{},"That sounds abstract until you list what depends on being able to tell:",[68,69,70,76,82,88],"ul",{},[30,71,72,75],{},[34,73,74],{},"Rate limits"," scoped per client stop meaning anything, because there's only one apparent client.",[30,77,78,81],{},[34,79,80],{},"Audit trails"," record the human, with no record that a service was involved at all.",[30,83,84,87],{},[34,85,86],{},"Anomaly detection"," loses its baseline: traffic from an automated component and traffic from a person at a keyboard have very different shapes, and merging them hides both.",[30,89,90,93],{},[34,91,92],{},"Revocation"," gets coarse. You can no longer cut off the service without cutting off the person.",[15,95,96,97,100,101,106],{},"Every one of those controls is keyed on the token's ",[19,98,99],{},"audience"," — who the token was minted for. Passthrough destroys that field's meaning. This isn't a policy preference; it's why the ",[102,103,105],"a",{"href":104},"\u002Fgarden\u002Fmcp-server-authorization","MCP specification forbids it outright"," rather than merely discouraging it.",[15,108,109],{},"So the service needs its own credential. That leaves exactly two shapes.",[49,111,113],{"id":112},"option-one-service-identity","Option one: service identity",[15,115,116],{},"The service holds a credential of its own. It authenticates to the backend as itself.",[15,118,119,120,123],{},"The backend sees ",[19,121,122],{},"the service",", cleanly and unambiguously. Audience checks work. Rate limits work. Revoking the service is one operation.",[15,125,126,127,130,131,134],{},"The cost is stated plainly: ",[34,128,129],{},"the human's trail ends at the service."," The backend knows \"this service did it\", not \"this service did it for Amir\". If you need to answer ",[19,132,133],{},"which person"," triggered a given backend call, you have to answer it from the service's own logs, and you have to trust those logs, and they're now a separate system from your backend's audit trail.",[15,136,137,138,142],{},"This option's real virtue is that it works everywhere. Every OAuth server does ",[139,140,141],"code",{},"client_credentials",". There is no feature to shop for.",[49,144,146],{"id":145},"option-two-delegation","Option two: delegation",[15,148,149,150,153],{},"The service exchanges the caller's token for a ",[19,151,152],{},"different"," token, minted for the backend, that carries both identities.",[15,155,156,157,163],{},"RFC 8693 (OAuth 2.0 Token Exchange) is the standard for this, and its mechanism is the ",[34,158,159,162],{},[139,160,161],{},"act"," claim",": the subject of the token is the human, and the actor is the service. Both are present in one token, and a backend that reads the claim sees \"X, acting through Y.\" Whether a given backend reads it is a separate question, taken up further down — the answer is not automatically yes.",[15,165,166,167,169],{},"The ",[139,168,161],{}," claim nests. A chain of services each add themselves, and the resulting token describes the whole path rather than flattening it.",[49,171,173],{"id":172},"delegation-is-not-impersonation","Delegation is not impersonation",[15,175,176],{},"This is the distinction most worth carrying away, because the two are frequently discussed as one thing and they have opposite properties.",[68,178,179,189],{},[30,180,181,184,185,188],{},[34,182,183],{},"Impersonation",": the service ",[19,186,187],{},"becomes"," the human. The resulting token says \"X\". The service's involvement is gone — not obscured, gone. Downstream, there is no way to recover the fact that a machine was in the path.",[30,190,191,194,195,198],{},[34,192,193],{},"Delegation",": the service acts ",[19,196,197],{},"as itself, on behalf of"," the human. The resulting token says \"X, via Y\". Both identities survive.",[15,200,201,202,205,206,209],{},"Mechanically, the difference between them is a ",[34,203,204],{},"single parameter",": whether the exchange request includes an ",[139,207,208],{},"actor_token",". Send it, and you get delegation. Omit it, and you get impersonation.",[15,211,212],{},"That's a startlingly small lever for a large semantic difference, and it's worth knowing which one a given implementation gives you by default — and, more to the point, whether it supports both at all. Several do not.",[49,214,216],{"id":215},"but-does-the-backend-read-it","But does the backend read it?",[15,218,219],{},"Putting the identity into the token and having something downstream act on it are different achievements, and the standard only guarantees the first.",[15,221,222,223,226,227,230],{},"This got measured rather than assumed: Zitadel 2.71.7 issuing tokens against Stalwart 0.16.17. The exchange itself works exactly as described above — ",[139,224,225],{},"sub"," is the human, ",[139,228,229],{},"act.sub"," is the service, and Stalwart accepts the token and acts as the human.",[15,232,233,234,236,237,240],{},"Then it stops. Stalwart never reads ",[139,235,161],{},". And a successful bearer authentication produces ",[34,238,239],{},"no authentication event at all"," — checked with a trace-level tracer — where a password login produces one. So on the backend side, a delegated request is indistinguishable from the person acting directly, and neither one is written down.",[15,242,243,244,246,247,250,251,254],{},"Worth being precise about what that is and isn't. It's not a defect, and it isn't carelessness: reading ",[139,245,161],{}," is a capability that simply isn't present, which is the ordinary state of most backends today. The RFC defines what a token may ",[19,248,249],{},"say",". It obliges no resource server to ",[19,252,253],{},"care",".",[256,257,258],"blockquote",{},[15,259,260],{},[34,261,262],{},"Delegation puts the identity in the token. Whether the backend reads it is a second question — and it often isn't asked.",[15,264,265],{},"The consequence lands directly on the choice this page is about. If you picked delegation to get an end-to-end audit trail, you've done half the work. The other half is confirming the backend uses what you sent. Where it doesn't, the trail has to be produced by your own service instead — which is precisely the cost option one was already charged for above.",[15,267,268,269,272,273,275],{},"So delegation's advantage over service identity is ",[34,270,271],{},"smaller than the earlier sections imply",", whenever the backend ignores ",[139,274,161],{},". What survives that discount is still real:",[68,277,278,284,290],{},[30,279,280,283],{},[34,281,282],{},"Short-lived tokens"," rather than a long-lived shared secret sitting in the service's configuration.",[30,285,286,289],{},[34,287,288],{},"Central revocation"," — cut the service off at the issuer instead of rotating a credential everywhere it was deployed.",[30,291,292,295],{},[34,293,294],{},"No backend credential in the service's config"," at all.",[15,297,298],{},"Those are security properties, not audit properties. If audit was the reason for reaching for delegation, check the far end before counting the win.",[49,300,302],{"id":301},"the-questions-this-actually-turns-into","The questions this actually turns into",[15,304,305],{},"Notice what happened. \"How should my service authenticate?\" resolved into two much more specific questions, both answerable before committing to anything:",[27,307,308,316],{},[30,309,310],{},[34,311,312,313,315],{},"Does my token issuer support RFC 8693 with an ",[139,314,208],{},"?",[30,317,318],{},[34,319,320,321,323],{},"Does my backend do anything with the ",[139,322,161],{}," claim once it arrives?",[15,325,326,327,330,331,254],{},"The first is a yes\u002Fno you can settle from documentation — and plenty of identity providers answer ",[19,328,329],{},"no"," while still advertising RFC 8693 support, because issuing a token for yourself and issuing a token that names two parties are different features filed under one RFC number. That's the subject of the next note: ",[102,332,334],{"href":333},"\u002Fgarden\u002Fquestions-to-ask-an-idp","Questions to Ask an IdP",[15,336,337],{},"The second is the one that's easy to skip, because the standard says nothing about it and the token looks correct either way. It can only be answered against the specific backend — and, as above, the answer is often no.",[49,339,341],{"id":340},"where-this-actually-stands","Where this actually stands",[15,343,344,347],{},[34,345,346],{},"Budding."," The model is settled and the vocabulary is standard — none of it is invented here. One end of it has since been measured rather than read: the Zitadel\u002FStalwart result above is a real test of a real pair, not a documentation claim. The deployment itself still hasn't happened, so most of the operational story is still ahead.",[15,349,350,351,354],{},"The honest summary, updated: service identity is always available and gives up the human's trail. Delegation preserves that trail ",[19,352,353],{},"in the token",", and constrains your choice of issuer considerably — but it only pays off in audit terms if the backend reads what it's sent. Pick knowing which you're paying for, and check the far end before assuming you got it.",{"title":356,"searchDepth":357,"depth":357,"links":358},"",2,[359,360,361,362,363,364,365],{"id":51,"depth":357,"text":52},{"id":112,"depth":357,"text":113},{"id":145,"depth":357,"text":146},{"id":172,"depth":357,"text":173},{"id":215,"depth":357,"text":216},{"id":301,"depth":357,"text":302},{"id":340,"depth":357,"text":341},"A service that calls a backend for a person needs two credentials — and the first must never become the second.",false,"md",{},true,null,"\u002Fgarden\u002Facting-on-a-humans-behalf",{"title":6,"description":366},"budding",[376,377],"security","identity","delegation","uKdI0phUDX6P87X_uCGwFwgfIRwYhscK0xZhZiTTZik",[381,627],{"id":382,"title":383,"body":384,"description":621,"draft":367,"extension":368,"meta":622,"navigation":370,"notice":371,"path":104,"seo":623,"stage":374,"stem":388,"tags":624,"topic":625,"__hash__":626},"garden_en\u002Fmcp-server-authorization.md","MCP Server Authorization",{"type":8,"value":385,"toc":613},[386,389,392,399,403,406,411,414,421,425,434,441,448,452,455,460,469,484,489,496,503,508,517,522,525,538,542,545,557,563,573,577,580,600,603,605,610],[11,387,383],{"id":388},"mcp-server-authorization",[15,390,391],{},"The common reading is \"MCP servers need OAuth\", followed by bolting OAuth onto every server someone writes. The specification says something considerably more specific, and the specific version is easier to implement.",[15,393,394,395,398],{},"Everything below is from the Model Context Protocol specification, ",[34,396,397],{},"revision 2026-07-28",". Quotes are verbatim, and the RFC 2119 keywords are the spec's own — the difference between MUST and SHOULD is doing real work here.",[49,400,402],{"id":401},"authorization-is-optional","Authorization is optional",[15,404,405],{},"Straight from the spec:",[256,407,408],{},[15,409,410],{},"\"Authorization is OPTIONAL for MCP implementations.\"",[15,412,413],{},"A server behind a reverse proxy that already authenticates, or one bound to localhost and reachable only by processes on that machine, is entitled to implement none of this. That's not a loophole; it's the design.",[15,415,416,417,420],{},"The question to ask isn't \"do I need auth?\" but \"",[34,418,419],{},"what's my transport, and what's my trust boundary?","\"",[49,422,424],{"id":423},"over-stdio-dont","Over stdio, don't",[256,426,427],{},[15,428,429,430,433],{},"\"Implementations using an STDIO transport ",[34,431,432],{},"SHOULD NOT"," follow this specification, and instead retrieve credentials from the environment.\"",[15,435,436,437,440],{},"Worth reading twice, because it's stronger than permission to skip the work — it's a recommendation ",[19,438,439],{},"against"," doing it.",[15,442,443,444,447],{},"The logic is that over stdio the trust boundary is the ",[34,445,446],{},"process boundary",". The client launched the server as a subprocess. It already controls the server's environment, its lifetime, and its file descriptors. Layering a token exchange on top of that relationship doesn't add a boundary; it adds ceremony across a boundary that was never crossed. Credentials come from the environment, because whoever set the environment is already the trusted party.",[49,449,451],{"id":450},"over-http-youre-an-oauth-21-resource-server","Over HTTP, you're an OAuth 2.1 resource server",[15,453,454],{},"This is where the obligations become concrete. An HTTP-transport MCP server is a resource server, and it owes four things.",[15,456,457],{},[34,458,459],{},"Publish protected resource metadata.",[256,461,462],{},[15,463,464,465,468],{},"\"MCP servers ",[34,466,467],{},"MUST"," implement OAuth 2.0 Protected Resource Metadata (RFC 9728)\"",[15,470,471,472,475,476,479,480,483],{},"In practice: serve ",[139,473,474],{},"\u002F.well-known\u002Foauth-protected-resource",", and include a ",[139,477,478],{},"resource_metadata"," pointer in the ",[139,481,482],{},"WWW-Authenticate"," header on a 401. That's how a client discovers which authorization server to go to without being told out of band.",[15,485,486],{},[34,487,488],{},"Validate the audience.",[256,490,491],{},[15,492,464,493,495],{},[34,494,467],{}," validate that access tokens were issued specifically for them as the intended audience\"",[15,497,498,499,502],{},"A token that is merely ",[19,500,501],{},"valid"," is not sufficient. It has to have been minted for this server. A server that accepts any well-signed token from a familiar issuer becomes a confused deputy for every other resource that issuer serves.",[15,504,505],{},[34,506,507],{},"Never pass the token through.",[256,509,510],{},[15,511,512,513,516],{},"\"The MCP server ",[34,514,515],{},"MUST NOT"," pass through the token it received from the MCP client\"",[15,518,519,520,254],{},"Forwarding the caller's token to a downstream API is prohibited, not discouraged. If the server needs to call something on the user's behalf, it needs its own credential — which is the entire subject of ",[102,521,6],{"href":372},[15,523,524],{},"Note how cleanly these last two fit together: if every resource server validates audience, and no server forwards tokens, then a token's audience always tells you truthfully where it was meant to go. Each rule is what makes the other one worth enforcing.",[15,526,527,534,535,537],{},[34,528,529,530,533],{},"Include ",[139,531,532],{},"scope"," in the challenge."," This one is a SHOULD, not a MUST — the ",[139,536,482],{}," header should name the scope required, so a client can request the right thing on its second attempt rather than guessing.",[49,539,541],{"id":540},"transport-obligations","Transport obligations",[15,543,544],{},"Independent of authorization, and easy to overlook because they aren't about tokens:",[256,546,547],{},[15,548,549,550,552,553,556],{},"\"Servers ",[34,551,467],{}," validate the ",[139,554,555],{},"Origin"," header on all incoming connections\"",[15,558,559,560,562],{},"An invalid ",[139,561,555],{}," gets a 403. This is what stops a web page the user happens to have open from driving a local MCP server through the browser — an attack that needs no credentials at all, because the browser helpfully supplies them.",[15,564,565,566,569,570,254],{},"Additionally: servers running locally ",[34,567,568],{},"SHOULD"," bind only to localhost rather than all interfaces, and there is to be exactly ",[34,571,572],{},"one endpoint path",[49,574,576],{"id":575},"the-actual-shape-of-the-decision","The actual shape of the decision",[15,578,579],{},"Reordered as a thing you can act on:",[68,581,582,588],{},[30,583,584,587],{},[34,585,586],{},"stdio transport"," → no authorization layer. Credentials from the environment. The spec recommends against anything more.",[30,589,590,593,594,596,597,599],{},[34,591,592],{},"HTTP transport"," → OAuth 2.1 resource server: RFC 9728 metadata, strict audience validation, no passthrough, ",[139,595,532],{}," in the challenge, ",[139,598,555],{}," validation, localhost binding when local.",[15,601,602],{},"Most of the confusion in this area comes from reading advice written for the second case and applying it to the first.",[49,604,341],{"id":340},[15,606,607,609],{},[34,608,346],{}," Sourced directly from one specification revision and quoted rather than paraphrased, so it's as reliable as that document — and it will age exactly as fast as the spec does. The revision is stamped at the top for that reason.",[15,611,612],{},"What this note doesn't cover: how any particular SDK implements the above, whether the client side holds up its end, or the dynamic client registration story. Those were not checked and aren't asserted.",{"title":356,"searchDepth":357,"depth":357,"links":614},[615,616,617,618,619,620],{"id":401,"depth":357,"text":402},{"id":423,"depth":357,"text":424},{"id":450,"depth":357,"text":451},{"id":540,"depth":357,"text":541},{"id":575,"depth":357,"text":576},{"id":340,"depth":357,"text":341},"Whether an MCP server needs OAuth at all depends on its transport — and over stdio the spec says it shouldn't have any.",{},{"title":383,"description":621},[376,377],"mcp-auth","z-V6MLWNhR2V4fupN2eyFYJchodqruriGx8djSrlAUE",{"id":628,"title":334,"body":629,"description":930,"draft":367,"extension":368,"meta":931,"navigation":370,"notice":371,"path":333,"seo":932,"stage":933,"stem":633,"tags":934,"topic":936,"__hash__":937},"garden_en\u002Fquestions-to-ask-an-idp.md",{"type":8,"value":630,"toc":917},[631,634,637,640,657,660,666,672,676,688,691,695,698,703,710,715,721,726,735,738,741,746,749,754,769,776,780,783,788,793,796,811,816,819,823,829,832,836,843,857,860,864,867,872,875,878,893,896,898,904,910],[11,632,334],{"id":633},"questions-to-ask-an-idp",[15,635,636],{},"The usual framing is a shootout: line up the self-hostable identity providers, compare features, pick a winner, deploy it. That framing quietly assumes the thing worth testing here — that one system should do the whole job.",[15,638,639],{},"The more useful move is to split the job first:",[27,641,642,647,652],{},[30,643,644],{},[34,645,646],{},"Who authenticates humans?",[30,648,649],{},[34,650,651],{},"Who issues tokens for services?",[30,653,654],{},[34,655,656],{},"Does the backend use what those tokens say?",[15,658,659],{},"The first two are different problems. The first is about authentication policy, credential strength, recovery flows, and how much you trust a password. The second is about token shapes, claims, exchange, and what a backend can prove about a request's provenance.",[15,661,662,665],{},[34,663,664],{},"They are allowed to have different answers."," Forcing both into one system means choosing a product that is inevitably stronger at one than the other, and then living with the weaker half. Once you accept two answers, each question gets to be decided on its own merits.",[15,667,668,669,671],{},"The third isn't a question about the identity provider at all — which is exactly why it gets skipped. You're shopping for an IdP, so you ask IdP questions. But an issuer that can express delegation buys nothing if the backend discards it on arrival, and that turned out to be the case here, measured rather than assumed: ",[102,670,6],{"href":372}," has the result. It doesn't change the choice below. It does thin out one of the arguments for it.",[49,673,675],{"id":674},"what-actually-forced-the-split","What actually forced the split",[15,677,678,679,681,682,685,686,254],{},"The concrete requirement was ",[102,680,378],{"href":372}," — a service acting on a person's behalf, with both identities visible to the backend. In RFC 8693 terms: token exchange ",[19,683,684],{},"with"," an ",[139,687,208],{},[15,689,690],{},"This turns out to be an unusually good discriminator, because \"supports RFC 8693\" is a claim many products make while implementing meaningfully different subsets of it.",[49,692,694],{"id":693},"the-evidence","The evidence",[15,696,697],{},"What follows was checked against each project's own documentation. Quotes are verbatim. Where something wasn't checked, that's stated rather than glossed.",[699,700,702],"h3",{"id":701},"kanidm","Kanidm",[15,704,705,706,709],{},"RFC 8693 support landed in ",[34,707,708],{},"v1.9.0 (2026-02-17)"," — but not in the delegating form. From its service-account documentation:",[256,711,712],{},[15,713,714],{},"\"Service accounts can exchange their API bearer token for OAuth2\u002FOIDC tokens (access\u002Fid\u002Frefresh) without user consent or interaction.\"",[15,716,166,717,720],{},[139,718,719],{},"subject_token"," must be the service account's own API token. And explicitly:",[256,722,723],{},[15,724,725],{},"\"actor_token is not supported for this flow.\"",[15,727,728,729,734],{},"Read carefully, this is ",[19,730,731,732],{},"effect-equivalent to ",[139,733,141],{},": a service obtains a token for itself. That's a genuinely useful feature. It is not delegation, and no configuration turns it into delegation, because the parameter that would express delegation isn't accepted.",[699,736,737],{"id":737},"authentik",[15,739,740],{},"Supports both forms:",[256,742,743],{},[15,744,745],{},"\"authentik supports both impersonation and delegation as defined by RFC 8693.\"",[15,747,748],{},"With a version floor:",[256,750,751],{},[15,752,753],{},"\"Delegation and on-behalf-of token exchange are available in authentik 2026.8 and later.\"",[15,755,756,757,760,761,764,765,768],{},"Two packaging facts, both checked directly: nixpkgs currently carries ",[34,758,759],{},"2026.5.6"," — ",[19,762,763],{},"below"," the release where delegation arrived — and there is ",[34,766,767],{},"no NixOS module"," for it. So adopting it means both waiting on (or driving) a package bump and writing the service definition yourself.",[15,770,771,772,775],{},"Operationally it's a Python application plus a separate worker process, with a stated minimum of ",[34,773,774],{},"2 cores and 2 GB RAM",". Its own documentation describes Compose as being \"for test setups and small-scale production\".",[699,777,779],{"id":778},"zitadel","Zitadel",[15,781,782],{},"Supports delegation, and the documentation reads as more settled — no beta qualifier in the current docs.",[256,784,785],{},[15,786,787],{},"\"currently only a valid access token or ID token are allowed as actor token\"",[256,789,790],{},[15,791,792],{},"\"The user represented by the actor token must have the impersonation permission set, or else the request will be rejected\"",[15,794,795],{},"That second line matters: delegation isn't ambient, it's a permission you grant. There are four permission levels along two axes (IAM\u002FOrg × Admin\u002FEndUser), so the authority to act on someone's behalf is itself scoped rather than binary.",[15,797,798,799,802,803,806,807,810],{},"Operationally: Go and PostgreSQL, roughly ",[34,800,801],{},"512 MB RAM"," for the service, a NixOS module at ",[139,804,805],{},"services\u002Fweb-apps\u002Fzitadel.nix",", package ",[34,808,809],{},"2.71.7",". It federates to generic OIDC providers, and is explicit about what that means for the resulting user model:",[256,812,813],{},[15,814,815],{},"\"All user profiles are managed within ZITADEL, allowing for a unified view of user identities regardless of the IdP used for authentication.\"",[15,817,818],{},"It also does passkeys natively per FIDO2\u002FWebAuthn.",[699,820,822],{"id":821},"keycloak-not-evaluated","Keycloak — not evaluated",[15,824,825,828],{},[34,826,827],{},"This was not checked."," Keycloak was excluded early on operational grounds — a JVM deployment, heavier than the rest of the estate — and its delegation capabilities were never investigated.",[15,830,831],{},"Stating that plainly matters. Keycloak is the most widely deployed option in this category and may well handle delegation perfectly. It isn't ranked here because no ranking was performed on it, and presenting an unexamined product alongside three examined ones would misrepresent how much work went into the comparison.",[49,833,835],{"id":834},"kanidms-own-axis","Kanidm's own axis",[15,837,838,839,842],{},"It would be easy to read the above as \"Kanidm fell short.\" That misreads the situation, and its own comparison documentation makes the point: it offers stronger authentication policy, ",[34,840,841],{},"WebAuthn attestation"," (which the others don't provide), Unix authentication integration, and a deliberate choice to implement its own database rather than depend on an external SQL server.",[15,844,845,846,849,850,852,853,856],{},"Those are real strengths, and they all sit on the ",[19,847,848],{},"first"," axis — authenticating humans well. None of them are weakened by the absence of ",[139,851,208],{},", because delegation is a question on the ",[19,854,855],{},"second"," axis.",[15,858,859],{},"Kanidm chose a design space on purpose. The requirement here simply grew outside it. That's a different finding from \"built badly\", and the distinction is the whole reason the two-question framing is worth adopting.",[49,861,863],{"id":862},"the-plan","The plan",[15,865,866],{},"Not yet implemented — this is the decision, not a report on living with it:",[15,868,869],{},[34,870,871],{},"Keep Kanidm as the identity source for humans. Add Zitadel alongside it for token issuance and delegation, federating to Kanidm for authentication.",[15,873,874],{},"Humans keep authenticating where they already do. Services get an issuer that can mint tokens naming two parties. Neither system is asked to do the thing it wasn't designed for.",[15,876,877],{},"Two reasons this is an addition rather than a migration:",[27,879,880,883],{},[30,881,882],{},"Real services already authenticate against Kanidm in production — the git forge and the fediverse server.",[30,884,885,888,889,254],{},[34,886,887],{},"The passkeys are domain-bound."," Moving would ask every user to re-enrol their hardware. That constraint deserves its own note: ",[102,890,892],{"href":891},"\u002Fgarden\u002Fwebauthn-credentials-are-domain-bound","WebAuthn Credentials Are Domain-Bound",[15,894,895],{},"The second reason is the one that would still apply even if the first didn't.",[49,897,341],{"id":340},[15,899,900,903],{},[34,901,902],{},"Seedling."," The decision is made; nothing is deployed. Most of the above is documentation-checking, which is exactly the kind of evidence that survives contact with reality least well — federation in particular tends to be where the interesting problems are, and none of them have been met yet.",[15,905,906,907,909],{},"One piece has since been measured rather than read: the exchange itself works end to end against the real backend, and that backend ignores the ",[139,908,161],{}," claim. The plan below is unchanged by it — the reasons for it were never only about audit — but one of its arguments is thinner than it looked.",[15,911,912,913,916],{},"Two known gaps, kept visible rather than quietly dropped: ",[34,914,915],{},"Keycloak was never evaluated",", and running two identity systems has an ongoing operational cost that hasn't been paid yet and therefore can't honestly be reported on. This note gets revisited once it has.",{"title":356,"searchDepth":357,"depth":357,"links":918},[919,920,927,928,929],{"id":674,"depth":357,"text":675},{"id":693,"depth":357,"text":694,"children":921},[922,924,925,926],{"id":701,"depth":923,"text":702},3,{"id":737,"depth":923,"text":737},{"id":778,"depth":923,"text":779},{"id":821,"depth":923,"text":822},{"id":834,"depth":357,"text":835},{"id":862,"depth":357,"text":863},{"id":340,"depth":357,"text":341},"Which identity provider?",{},{"title":334,"description":930},"seedling",[376,377,935],"self-hosting","identity-provider","im6mTU-ZGplSH07m1Rrp3imlket6iGZ9rJoraVNuU_w",1789414694558]