Repository navigation
Protect any keytab or file; user vaults with kuipa seal - #6
Merged
Merged
Conversation
`kuipa protect --keytab F` seals a service's or a user's keytab for its
own principal, and `kuipa protect --file F [--keytab K]` seals any file for
K's principal (the host's by default). Each is sealed into `F.sealed`,
unsealed into tmpfs under /run/ipa/kuipa with the original mode and owner,
and F becomes a symlink to that copy, as /etc/krb5.keytab does in file
mode. Protecting a path again reseals what is there now, so keys written
by ipa-getkeytab through the symlink can be resealed. `unprotect` puts the
plaintext back and retires the bindings.
Bindings outside a host rekey travel in a new join-protocol operation,
`POST /ipa/ku/register/{kid}`, authenticated by the principal's own ticket
and answered under its Kerberos binding. A principal's entry holds two
binding sets: its keytab's, replaced at every seal, and its files',
added to and retired one seal at a time, so a host rekey leaves files
sealed for the host openable. kuipad finds hosts by FQDN and other
principals by name under cn=accounts, and recovery accepts either set.
Sealed keytabs keep their bytes exactly (a re-encoded keytab changes its
name type); files use a new `IPSF` envelope. `kuipa-unseal --all` (now the
unit's command) unseals the host keytab and every recorded item, and
`kuipa-unseal --sealed S [--out F | --memfd]` unseals one, which is the
per-service gssproxy loader. `status`, `check` and `leave` cover the
recorded items.
DESIGN §7.5 describes it; the schema needs `ipaKUEscrowFileBinding` and
ACIs for the escrow attributes on service and user entries (TODO).
Signed-off-by: Alexander Bokovoy <ab@vda.li>
The keytab_loader change is submitted as gssapi/gssproxy#135: the memfd loader into a MEMORY keytab, SIGHUP reload, retry with backoff, and core dumps disabled while a loader is configured. TODO keeps it open until it merges, and spells out what kuipa protect then does with it. Signed-off-by: Alexander Bokovoy <ab@vda.li>
`--out -` writes what --sealed holds to standard output, unchanged and alone (traces stay on standard error), and refuses a terminal unless --force. Any other --out is written atomically, so it must be a regular file or nothing yet, outside /dev: before, `--out /dev/stdout` run as root replaced the /dev/stdout link with a file and printed nothing. Both are checked before a server is contacted. Signed-off-by: Alexander Bokovoy <ab@vda.li>
DESIGN §7.6: users seal their own files, without root, to a vault whose policy (within realm limits) names the factors an unlock needs: a device key of a given level (passphrase file, passkey-wrapped, PKCS#11-wrapped or PKCS#11-native ML-KEM), the owner's ticket, a passkey assertion. Sealing is offline: every binding carries a commitment to its principal and vault that the server checks, which closes the hole of presenting another user's binding under one's own vault. Files hold a server share and a device share, so IPA administrators cannot read them; answers are encrypted to a registered device. Re-authentication and offline sessions, an unlock counter in the anchor exchange's CRDT, and host and service escrow as machine vaults on the same request. Open questions are listed in §7.6.12. Signed-off-by: Alexander Bokovoy <ab@vda.li>
The command is `kuipa seal` (the server object stays a vault, distinct from ipa vault-*); offline-session keys are sealed per server in the vault entry; registered sealing stays a policy option beside the offline default; any of the owner's IPA passkeys satisfies the passkey factor. Passkeys are used without a browser: kuipa talks CTAP through libfido2 as SSSD's passkey_child does, so the verifier copied from ahdapa's relying party code takes a raw clientDataHash. Signed-off-by: Alexander Bokovoy <ab@vda.li>
An offline session hands a key to the client, so its end is no longer the server's to enforce. Both the vault policy and the realm limits now default to off: an administrator opts the realm in, then a user opts a vault in. Signed-off-by: Alexander Bokovoy <ab@vda.li>
docs/sealing.md walks through what users would do with the planned `kuipa seal`: secrets in a home directory, piping into programs, passkey and token devices, several machines, a lost laptop, working-day and offline sessions, a break-glass secret, leaving the organisation, what protects against whom, and what it does not do. Writing it showed that a device's level must come from its attestation, not its type: a software PKCS#11 token or an unattested passkey is an `any` device, which is also how unattended jobs fit. Signed-off-by: Alexander Bokovoy <ab@vda.li>
docs/machines.md: anchors baked into images, autoscaler joins with vouchers (bound to the instance where user-data is readable), staged bundles for instances without network at first boot, sealed host and service keytabs and application secrets against snapshots, Clevis tang-pqc volumes against kuipad, termination hooks, and what the planned vTPM factor and kuipa seal add. Each use case says whether it is available, waits for FreeIPA, or is planned. The guide states the limit of sealing in a cloud: a recent snapshot restored inside the network opens while the host is enabled. It also avoids promising that host-disable retires sealed files: FreeIPA's host-disable does not set nsAccountLock, which is what kuipad checks. The gssproxy example now points at the upstream pull request. Signed-off-by: Alexander Bokovoy <ab@vda.li>
ipa host-disable and service-disable remove keys and certificates but leave the entry unlocked, and a principal without keys may still get tickets with PKINIT. Missing keys are therefore no proof of a disabled principal; nsAccountLock is, as ipa-kdb refuses every ticket for a locked entry. DESIGN §7.2 and §7.5 now say so, the cloud guide no longer promises that host-disable retires sealed files, and TODO asks FreeIPA's disable commands to set the lock. Signed-off-by: Alexander Bokovoy <ab@vda.li>
On IPA-enrolled Fedora and RHEL a PAM login gives the user a ticket, and sshd obtains one after a public-key login through protocol transition and optional S4U2Proxy, within the realm's delegation rules. The cloud guide now says so instead of suggesting ssh -K, which puts an unrestricted TGT on a machine whose root may not be trusted. DESIGN §7.6 notes that a delegated ticket attests the host's vouch, so the host keytab should be sealed, and leaves open whether a policy can refuse delegated tickets. Signed-off-by: Alexander Bokovoy <ab@vda.li>
The PAC's S4U delegation info lists the services a ticket passed through, so kuipad can tell a delegated ticket from one the owner got with their own credentials. ticket.delegation is allow (the default, so SSH public-key logins work), refuse, or via a list of services the ticket may have transited. DESIGN §7.6, the cloud guide and the sealing guide describe it. Signed-off-by: Alexander Bokovoy <ab@vda.li>
A server that authorises recovery by data the binding context carries cannot see that context: it is mixed into the key on the client only. provision_committed now also returns HKDF(ss, "<domain> commit" ‖ alg ‖ kid ‖ H(ct) ‖ data), and OpenedRequest::verify_commitment checks it after opening, so a client cannot present someone else's binding under data it is authorised for. The Tang wire format is unchanged. This is phase 1 of the user vault work (DESIGN §7.6); TODO lists the phases. Signed-off-by: Alexander Bokovoy <ab@vda.li>
Every slot of a sealed keytab or file now carries a tang-pqc commitment to IPCV(principal, vault), where the vault is the machine vault derived from the principal and the kind (DESIGN §7.6.8). The recovery request names the vault and carries the commitment; the server checks it after opening, then the binding registered in that kind's set only. Before, only the binding context, mixed in on the client, stopped a principal on whose entry someone had registered the same binding hash. This is phase 2 of the vault work: one recovery path that user vaults will join. No deployment exists, so IPSK, IPSF and IPEQ change in place. Signed-off-by: Alexander Bokovoy <ab@vda.li>
kuipa-enroll's vault module holds the policy (factors, ticket delegation,
expiry, sealing, sessions), the realm limits, registered devices, the
vault descriptor signed under the server's delegation, the owner's vault
operations, the two-share file format (IPUF: a server share released
through any escrow key, a device share per registered device), and
answers encrypted to the device the request names (IPVA).
The unified recovery request gains the device and the owner's GSS-API
token, bound to the request. For a user vault the server checks the
commitment, the vault and its owner, expiry, the ticket (with its
delegation against the policy), and the device and its level, then
answers to the device. POST /ipa/ku/vault/{kid} runs owner operations
through the join exchange, authenticated by the owner's ticket and
answered with signed descriptors.
Passkeys, unlock limits, sessions and registered sealing are refused
until their phases land. Staging moves out of EnrollServer::handle into
its own method. This is phase 3; the file magic is IPUF, as IPSV was
taken.
Signed-off-by: Alexander Bokovoy <ab@vda.li>
kuipad keeps one entry per vault under cn=vaults,cn=kuipa,cn=etc (owner,
name, policy and devices as JSON, state), reads the realm's limits from
cn=vault-limits (defaults when absent), accepts owners' tickets for vault
operations and the ticket factor, and answers whether an owner is locked.
POST /ipa/ku/vault/{kid} is routed. The KDC-backed test directory keeps
vaults in memory, so the real-KDC test and the playground can use them.
Phase 4 of the vault work. The schema joins the FreeIPA TODO.
Signed-off-by: Alexander Bokovoy <ab@vda.li>
kuipad does not read the PAC's S4U delegation info yet, so it cannot tell a delegated ticket from one the owner obtained. Accepted::transited is now None in that case, and only ticket.delegation = allow accepts it: refuse and via refuse rather than mistake every ticket for an undelegated one. The PAC work is parked on vault-pac-delegation. Signed-off-by: Alexander Bokovoy <ab@vda.li>
Runs as the user, with their ticket. The realm comes from a new world-readable realm.json written beside the root-only state, the pinned anchors and the CA; the user's directory holds the device key (the ML-KEM seed under PBKDF2-HMAC-SHA-512 and AES-GCM, IPDK), each vault's last signed descriptor and the servers' signed advertisements, so files seal offline while the descriptor is within the realm's age limit, which descriptors now carry. Commands: create, policy, device add|remove, file, open, reseal, show, list, revoke. open --out follows kuipa-unseal's rules through a shared output module, and with --out - kuipa prints nothing else on standard output. The real-KDC test and the playground run a user's vault end to end. Phase 6 of the vault work. Signed-off-by: Alexander Bokovoy <ab@vda.li>
Unlock limits: every release is counted on the vault entry, one '<server> <count>' value per server in ipaKUVaultUnlocks, each server replacing only its own, so replication never conflicts and the total is the sum; a server refuses once the sum reaches max_unlocks. Re-authentication sessions: the vault operation unlock, authenticated by the owner's ticket, answers with a token for one registered device, signed under the server's delegation so any server checks it. A request carrying it stands in for the ticket. Sessions need the device factor, since the answer is encrypted to the device; a token alone opens nothing. kuipa seal unlock keeps the token and the unwrapped device key in the user's runtime directory (tmpfs) until the session ends or kuipa seal lock, so a session spares the passphrase too. The counter is per-server LDAP values instead of the anchor exchange's CRDT, as agreed. Phase 7 of the vault work. Signed-off-by: Alexander Bokovoy <ab@vda.li>
A vault whose policy and realm allow offline sessions gets a session key per epoch, made by a server and sealed to every server's escrow key: each kuipad publishes its escrow public key on its cn=masters entry, and a server that can open an epoch key seals it to every published key that has no slot yet, so later replicas catch up. Descriptors carry each epoch's public key only. kuipa seal file --session seals the server share to the current epoch key (IPUS) instead of through the escrow keys; kuipa seal unlock --offline, authenticated by the ticket and counted as one unlock, releases every epoch key so far encrypted to the device (IPEK), which the client keeps with the unwrapped device key in the runtime directory and uses to open session-mode files with no server at all. The playground opens one with both servers down. Registration and vault operations no longer draw on the per-host rate limit bucket: they are only ever Kerberos-authenticated, and a host's users must not share one allowance. Phase 8 of the vault work. Signed-off-by: Alexander Bokovoy <ab@vda.li>
kuipa seal device add VAULT --pkcs11 URI [--module PATH] registers the public half of an ML-KEM key generated on a token; the private half never leaves it. kuipa loads OpenSSL's pkcs11-provider into a library context of its own (its configuration written to the user's runtime directory, since native-ossl's helper names the module as other distributions do), passes the PIN as the URI's pin-value, and decapsulates on the token. The host remembers the module and URI; the PIN takes the passphrase's place everywhere, and a session cannot cache a token key, so opening in a session still asks for it. No attestation is verified yet, so token devices register at level any. Tested against a kryoptic token, made with pkcs11-tool, in a unit test and in the real-KDC test. Phase 9a of the vault work. Signed-off-by: Alexander Bokovoy <ab@vda.li>
With sealing = registered, a server releases a file's share only while the file's binding hashes are registered on the vault (ipaKUVaultBinding, left out of descriptors, at most 4096). kuipa seal file registers them with the new vault operation register after sealing; kuipa seal reseal retires the old file's, and kuipa seal retire retires a file's outright, so every copy of it, wherever it is, stops opening. --sealing registered sets it on a policy. Signed-off-by: Alexander Bokovoy <ab@vda.li>
|
Sorry @abbra, your pull request is larger than the review limit of 150,000 diff characters |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends keytab protection from the host keytab to any keytab and any file, and adds user vaults: users seal their own files with
kuipa seal, under a policy the realm's servers enforce.Protecting keytabs and files (DESIGN §7.5)
kuipa protect --keytab Fseals a service's or user's keytab for its own principal;kuipa protect --file F [--keytab K]seals any file for K's principal (the host by default). Bindings register outside a rekey throughPOST /ipa/ku/register/{kid}, authenticated by the principal's own ticket.kuipa-unseal --all(now the unit's command) restores everything at boot;--sealed S --out F|-|--memfdunseals one item, which is also the per-service gssproxy loader (Add dynamic keytab loader gssapi/gssproxy#135).--out -writes the plaintext alone to standard output and refuses a terminal; other--outtargets must be regular files outside/dev. Before this,--out /dev/stdoutrun as root replaced the/dev/stdoutlink.One recovery path with commitments
provision_committed,OpenedRequest::verify_commitment). Every recovery request names a principal and a vault, and the server checks the binding was made for them. Before, only the client-side binding context stopped a principal on whose entry someone had registered the same binding hash.User vaults (DESIGN §7.6, docs/sealing.md)
cn=vaults,cn=kuipa,cn=etc. Each has a policy: factors (device, ticket), expiry, unlock limit, sessions and sealing mode. Users create vaults themselves within realm limits.IPDK: PBKDF2-HMAC-SHA-512 and AES-GCM; native-ossl exports no encrypted PKCS#8 for ML-KEM keys);$XDG_RUNTIME_DIRuntil the session ends orkuipa seal lock.--sealing registered, withresealandretirekilling old copies.allowaccepts, until the PAC delegation info is read. That work is parked onvault-pac-delegation, using pidl-rs-generated NDR.Docs
docs/sealing.md: user use cases.docs/machines.md: cloud VM use cases.host-disable/service-disablesettingnsAccountLock, and the remaining work.Not in this PR
hardware-wrappedandhardware-boundlevels: every device registers atany.LdapDirectoryagainst a real FreeIPA server; already in TODO for the rest of kuipad.Testing
All
contrib/ci/local-ci.sh alljobs pass: