Kerberized MCP Server with Delegation A reference implementation of a Kerberized MCP server for FreeIPA remote-SSH development environments has been published, using SPNEGO authentication against a keytab and deny-by-default authorization by FreeIPA directory group so that developers authenticate with the Kerberos ticket they already hold at login and no passwords, API keys or per-developer secrets are stored. The project, described as under heavy development with names, installer flags and file layouts still changing between commits, ships a server on the official Python SDK behind nginx, an installer that creates the service account, venv, keytab, systemd unit, nginx vhost and certificate, and a client kit including a stdio bridge, the mcp-krb launcher, krb-fetch and krb-git. The server logs every tool call as an audit line and optionally supports constrained delegation so a tool can act downstream as the caller, with a known rough edge that the Firefox installed by setup.ps1 inside WSL for Windows workstations is not yet stable. Reference Implementation of a Kerberised MCP Server for Multi-OS FreeIPA Remote-SSH VS Code Development Environments Kerberos single sign-on for MCP against FreeIPA: authenticate with the ticket a developer already holds from logging in, authorize by directory group, and keep no passwords, API keys or per-developer secrets anywhere. Status: under heavy development. Names, installer flags and file layouts still change between commits, so read the log before upgrading a deployment. Known rough edge: the Firefox that setup.ps1 installs inside WSL for Windows workstations is not stable yet. The bridge, wslssh , wslgit , krb-fetch and krb-git do not depend on it. The server's optional policy editor. Each tool maps to the FreeIPA groups that may call it and, if it may act downstream as the caller, to the one service it may reach. This is a system you deploy, in two halves, with one hook for your own code. It is not a library to import, and it is not an MCP client. You run the server on one host in your FreeIPA realm, hand the client kit to every workstation, and put your tools in one site file that this repository never carries. | Part | What you get | Where it runs | In the tree | |---|---|---|---| | The server | a Kerberised MCP server on the official Python SDK, behind nginx. Every request is authenticated offline by SPNEGO against a keytab; every tool call is authorized deny-by-default by FreeIPA group; every decision is an audit line. Optional: a tool may act downstream as the caller constrained delegation , and a browser editor for the policy pictured | one Linux host in the realm | server/ | | Its installer | one script that creates the service account, venv, keytab, systemd unit, nginx vhost and certificate, then publishes the client kit; a read-only verifier that checks the result | the same host | server/install/ | | The client kit | a stdio bridge that mints a fresh Negotiate token per request, the mcp-krb launcher an MCP client is pointed at, krb-fetch for byte-exact files and krb-git for git over Kerberos, and the reverse bridge: a shared dev host that holds no ticket uses your workstation's through a socket forwarded by ssh -R | every workstation and shared dev host | client/bridge/ | | Workstation provisioning | setup.sh Linux: enrol, then install , setup.ps1 Windows without domain join: WSL2, Kerberos ssh, VS Code Remote-SSH, a browser that can SSO, the bridge , setup-macos.sh , uninstallers driven by install manifests, and a provisioning web page the installer serves | workstations | client/ , client/web/ | Your own tools go in one Python file, loaded through MCP SITE TOOLS , each declaring the groups that may call it. The page's site-specific sections go in one HTML fragment, through CLIENT SITE SECTIONS . Neither lives in this repository, so a deployment never maintains a fork of a file it did not write. Behind all of it sits SECURITY.md https://github.com/overpassconnect/mcp-krb-server/blob/main/SECURITY.md , the review with the threat model, ranked findings and deployment checklist, and a hermetic test suite that runs on Windows and Linux with no KDC. What that gives a team, concretely: - A developer types one password, at login. From then on an MCP client such as Claude Code reaches the internal server with nothing to configure and no secret stored anywhere. Disabling the account in FreeIPA ends the access. - The server sees the real principal on every call, decides by directory group, and logs who did what under their own name. - A tool can call CI, a forge or any other Kerberised service as the person who asked, so the downstream system attributes the action to them and bounds it by their permissions there. - On a shared dev host with no ticket, the same things work: reaching the MCP server, fetching a file, and git clone, pull and push, all through a socket the workstation forwards. No credential is copied to the host. - A Windows workstation that is not domain-joined gets passwordless ssh, VS Code Remote-SSH and single sign-on in a browser, through WSL2. What it assumes: FreeIPA or another MIT Kerberos realm with an LDAP directory you can adapt the group lookup to , Linux hosts for the server and the shared machines, and an MCP client that speaks stdio. The tools it ships are stubs; the scaffolding around them is the product. MCP has no enterprise single-sign-on story. Claude Code's MCP client can attach an OAuth flow or a fixed header to its requests, but it cannot speak HTTP Negotiate/SPNEGO, the standard way a browser or a command-line tool proves a Kerberos identity to a web service. So it cannot authenticate directly to a Kerberized internal MCP server. This repository is a worked, reviewed answer for FreeIPA shops: it closes that gap using the Kerberos ticket a developer already holds from logging in to a FreeIPA-enrolled machine. No passwords, no API keys, no per-dev secrets. It is a reference implementation. The MCP tools it ships whoami , list projects , restart service , trigger build are stubs; the value is the authentication, authorization, delegation and deployment scaffolding around them, meant to be read, reviewed and adapted to your own tools. Your own tools go in a separate file that this repository never carries, loaded through MCP SITE TOOLS see Configuration configuration , so a deployment does not end up maintaining a fork of a file it did not write. A tool can also act on behalf of the caller. One of those stubs, trigger build , shows the delegation path: a tool calls a downstream Kerberized service as the human who invoked it, so the downstream logs the real person rather than a shared service account. It is off by default, reaches any Kerberized service CI is only the example , and has the one genuinely subtle security story here. Its own section, On-behalf-of delegation on-behalf-of-delegation , covers it. One idea holds this together: a user credential exists in exactly one place, the workstation. Every other machine either has its own machine identity, or has nothing at all. Nothing is copied, forwarded or minted to make a second you. ┌────────────────────────────────────────┐ │ FreeIPA: KDC · DNS · CA │ │ │ │ issues TGTs to people │ │ keytabs to machines │ │ the CA that signs every host │ └────────────────────────────────────────┘ ▲ ▲ ▲ ① kinit │ ② keytab │ ③ ca.crt │ plain HTTP, TGT │ at enrol │ to a new │ hash checked │ │ machine │ out of band ╔═════════════════════╪════════════════╪════════════════╪═════════════════╗ ║ WORKSTATION ┘ │ │ ║ ║ the ONLY place a user ticket lives │ │ ║ ║ on Windows: inside WSL ║ ║ ║ ║ MCP client ───stdio──▶┌────────┐ ║ ║ │ │ bridge │───── ④ SPNEGO ──────────────────┐ ║ ║ │ runs └───┬────┘ │ ║ ║ ▼ │ ⑦a --listen │ ║ ║ krb-fetch ──┐ ▼ │ ║ ║ krb-git ────┤ ~/.mcp-krb.sock 0600 │ ║ ║ ├──▶ ~/.mcp-krb-fetch.sock 0600 ⑦b │ ║ ║ └──▶ ~/.mcp-krb-git.sock 0600 ⑦c │ ║ ║ ▲ │ ║ ║ ssh ────── ⑤ ─────┐ │ │ ║ ╚══════════╪═══════════╪═══════╪══════════════════════════════════════╪═══╝ │ │ ║ │ │ │ ║ ssh -R, one per socket │ │ │ ║ the SOCKETS are forwarded, │ │ │ ║ never the credential │ │ │ ║ │ ╔══════════╪═══════════╪═══════╪══════════════════════════════════════╪═══╗ ║ SHARED │ DEV HOST ║ │ ║ ║ ◀────────┘ ⑤ ssh in ▼ │ ║ ║ /run/user/N/mcp-krb.sock ◀── ⑦a │ ║ ║ /run/user/N/mcp-krb-fetch.sock ◀── ⑦b │ ║ ║ /run/user/N/mcp-krb-git.sock ◀── ⑦c │ ║ ║ ▲ ▲ │ ║ ║ stdio │ │ asks the workstation │ ║ ║ MCP client ──▶ remote bridge krb-fetch · krb-git │ ║ ║ │ ▲ │ ║ ║ └────────── runs ──────────────────┘ │ ║ ║ │ ║ ║ has a HOST keytab. NO user ticket. nothing here to steal. │ ║ ╚═════════════════════════════════════════════════════════════════════╪═══╝ │ │ │ ⑧ fetch a file, byte-exact · clone, pull, push │ ④ ▼ ▼ ┌────────────────────────────────┐ ┌──────────────────────────────┐ │ any Kerberised service │ │ the MCP server │ │ git · CI · wiki · artifacts │◀─ ⑥ ───│ 401 on every path │ │ │ on-be- │ holds a SERVICE keytab │ │ whatever your shop runs; this │ half-of│ your tools live here │ │ repo assumes none of them │ │ refuses a forwarded TGT │ └────────────────────────────────┘ └──────────────────────────────┘ ┌────────────────────────────────┐ │ the provisioning page │ the one deliberately anonymous surface: │ served by this installer │ a machine that is not enrolled yet has │ client scripts, anonymous │ no ticket, so the bundle it needs in │ on purpose │ order to enrol cannot sit behind SPNEGO └────────────────────────────────┘ ① kinit The only step involving a human secret, on the only machine holding one. ② Host keytabs Each enrolled machine gets its own identity. That proves a machine is itself; it says nothing about who you are and cannot be used to become you. A Mac never does this, which is why macOS leaves the realm untouched where Linux does not. ③ CA bootstrap A new machine trusts nothing, so it fetches the realm CA over plain HTTP and checks it against a SHA-256 obtained elsewhere. That comparison is the whole check, which is why the hash must not come from the same infrastructure that serves the certificate. ④ MCP The client speaks stdio to the bridge; the bridge speaks SPNEGO to the server. The ticket never moves, because the bridge is already where it is. ⑤ SSH GSSAPI with GSSAPIDelegateCredentials no . Your ticket is not forwarded, which is why a shell on a shared host has no credentials. Design, not gap. ⑥ On-behalf-of A tool can act as you against a downstream Kerberised service using constrained delegation with a narrow evidence credential, refusing a forwarded TGT. The example tool targets CI; the mechanism cares about none of that. See On-behalf-of delegation on-behalf-of-delegation . ⑦ Forwarded sockets --listen and --fetch-listen serve MCP and fetching over 0600 Unix sockets. ssh -R forwards them to a shared host, where mcp-krb-remote-bridge.py joins a client's stdio to them. The far end holds no credential, imports no crypto, and loses the channel when the session ends. ⑧ Fetching a file --fetch writes a URL to disk over SPNEGO, for content that must arrive byte-exact and therefore must not pass through a model. On a shared host it goes via ⑦b, so the GET and the allowlist both happen where the ticket is. Fair question, since the assistant already has a shell and your ticket, and could call an internal API itself. Authorisation cannot be enforced on the client. A tool that runs on your machine is a tool you can edit. "Only this group may trigger a build" living in a script on a workstation is a suggestion; behind an authenticated server it is a rule, because the check happens somewhere the caller does not control. Audit needs a chokepoint. Every call is recorded against the Kerberos principal that made it, allowed or denied. Fifty workstations calling an API directly produce no such record. Delegation needs a service principal. Acting as you against another service, without holding your TGT, requires an identity the realm knows and rules it enforces. A shell script cannot be granted that, and should not be. And the corollary: things you could already do belong in the shell, not behind a tool. krb-fetch and krb-git are deliberately not MCP tools. The assistant already has your ticket, so wrapping an HTTP GET or a git pull in a server call would add a hop, a schema and an audit line while changing nothing about what is possible. The test each tool should pass is: could the caller do this themselves, unlogged, if the tool did not exist? When the answer is yes, it does not belong here. Worth stating plainly, because shared hosts are the case they exist for and several people at different privilege levels may be logged into one at once. The sockets are 0600 , so an unprivileged peer cannot use them. Root on that host can, while your session is open , which on a box where colleagues hold sudo means those colleagues. After you disconnect the socket file remains but nothing answers it. So the exposure is bounded by your session rather than by a ticket lifetime. The alternative, running kinit on the shared host, leaves a ticket cache that root there can read and use to become you everywhere in the realm, for its full lifetime, still valid after you log out. It is structurally ssh-agent forwarding, with a narrower grant: agent forwarding gives SSH-to-anywhere, these give one service and one allowlist. The operational rule that follows is to avoid mixing privilege levels and sudo on one host. krb-git adds one hop to this picture, a loopback port, because git cannot open a Unix socket. A port is reachable by every user on the host, so the forwarder behind it checks the uid of each connection against the kernel's TCP table and refuses anyone else. The port grants nothing the socket did not. A developer logs in to their workstation. That is the only time anyone types a password. From then on Claude Code reaches the internal MCP server with no further prompt and nothing to configure: klist a ticket is already there, put in place at login claude mcp list internal-tools: connected The server learns who they are on every request, decides what they may call from their directory group membership, and records the decision under their real name. Because nothing was set up with a shared secret, there is nothing to rotate, leak, or hunt down later: removing the account in FreeIPA removes the access. server/install/run.sh is the installer, and it covers every step so nothing has to be done by hand: the service account, the --system-site-packages venv, the code deploy, the keytab retrieval and permission contract, mcp-server.service , the nginx vhost, the certificate, and the certbot deploy hook. It is idempotent, so re-running it converges, and it refuses to proceed on a value it cannot resolve rather than defaulting to a placeholder. Two prerequisites are human on purpose, because they consume admin credentials the installer deliberately refuses to hold it preflights for both and stops with the exact command if either is missing : 1. The host is enrolled in FreeIPA ipa host-add with a one-time OTP, then ipa-client-install on the box . 2. The IPA service principal HTTP/