Repository navigation
Conversation
…ped through The check was a substring test on the RFC4514 form of the CSR subject and did not look at the signature at all. Both matter more than they look. The subject test accepted a CSR whose CN merely started with the callsign (CN=OTTER1X for OTTER1), and accepted one that carried the callsign in some other attribute while the real CN was something else entirely (OU=CN=OTTER1,CN=ADMIN). The CA signs the request as presented and the mTLS edge authorises on the CN it reads from the certificate, so the second case issues a working identity for a callsign the requester was never granted. Compare the parsed CN attributes instead, and require exactly one equal to the callsign. Other attribute types are still allowed: an MDM filling a subject template may add O or OU and nothing downstream reads them. The missing signature check meant we had no proof the requester held the private key. Worse, an unverifiable request was still passed to the CA, which rejects it only after Person.create_with_cert has committed the Person row, spending that callsign for good. Adds tests/test_csr_utils.py. Every negative case in it was accepted before.
CN=OTTER1X passed for OTTER1, and OU=CN=OTTER1,CN=ADMIN passed while the cert the CA issued said ADMIN. Now compares parsed CN attributes, and checks the signature so an unverifiable request never reaches the CA.
…on-rasenmaeher-api into fix/verify-csr-exact-subject
Enrolling three hundred phones through an MDM needs something to approve them, and the proof of concept did it by holding an admin certificate that could create and revoke anyone. This is the narrow version of that capability. An admin plans a device by calling /enrollment/init with mdm=true. The row is an ordinary pending enrollment carrying a marker, so it lists, locks and audits like every other one, and no new storage appears. No JWT is issued for it: that token is a live credential for the callsign, the device will never use it, and issuing it also overwrote the planning admin's own session cookie. The device's SCEP agent then calls /enrollment/accept with the CSR the device generated. It authenticates with its own client certificate from the deployment CA, and RM_MDM_AGENT_CNS says which CN may do this. That list is deliberately not the kraftwerk product CNs: those skip role checks, can have any CSR signed and cannot be revoked at the edge, which is far more than this needs. Empty by default, so the path does not exist until a deployment configures it. Approving a planned device by hand is refused with 409. It would generate the keypair here and the callsign would be spent, leaving the phone unable to ever have it. Three things that would otherwise be silent: - The claim is one conditional UPDATE, so two devices racing for a callsign produce one winner, and the row is re-read before approve() because approve() reads the CSR off the instance and would otherwise generate a keypair here. - Issuing is a network call to the CA. If it fails the claim is released so the next attempt can have the callsign back. - A repeat of a request we already answered returns the same certificate rather than 'callsign already taken', because the reply can be lost on the way back to the MDM. If the Person exists but has no certificate the callsign was spent by a failed issue and the agent is told so. Refusals all carry one detail so the agent cannot enumerate callsigns; the real reason goes to the audit log. Needs the verify_csr fix in the parent commit: without it the CSR subject is matched by substring, which is the only subject gate on this path.
A certificate carrying the agent CN is accepted as the agent on sight, with no Person, role or product check, so anyone issued one could complete every planned device enrolment. Product CNs were already reserved here; these were not, and the callsign is caller-chosen on the unauthenticated invite-code route.
Both a planned device and a person waiting for approval sit in the list as PENDING, but the device is waiting for its own CSR through the agent and approving it by hand is refused, so without the marker the approve-users queue fills with rows nobody may act on.
The clause the MVP plan gates on. A callsign is spent the moment it is used and is never released, so if both devices got past the claim one would hold an identity the other also has, and the loser could never be enrolled under any name it was promised.
The signer picks its profile from the request's own extended key usage, and nothing replaces the names it carries, so a request asking for server authentication was issued one by the deployment CA. On this path the request arrived from the public SCEP endpoint and its contents are entirely the caller's choice. A genuine MDM device request asks for none of this.
The SCEP endpoint is public and its challenge is not a secret, so the last gate was a callsign: a short human-chosen string anyone can guess. The MDM now carries <callsign>@<approvecode> in the one per-host field it can substitute into a subject, and the code is what says this device was assigned this callsign. Refused before anything is claimed, so guessing cannot spend a callsign either.
For a person the code is a secret they tell an admin, and listing it would let any admin approve anybody without being told anything. For a device it is the opposite: the admin has to type it into the MDM for the device to enrol at all, and nobody else ever learns it. Withholding it there meant an admin who plans a hundred devices could only read the codes once, at the moment of creation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The challenge is not a secret and is no longer treated as one. It sits in the MDM's own configuration and the MDM hands it to every device; what decides whether a device may have a callsign is the per-enrolment code in its certificate request. Admin only all the same, because nobody else has a reason to read it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Version resolved to 1.20.0: a feature branch bumps the minor, and it is ahead of main's 1.19.3 either way. rmsettings keeps main's callsign_validity_secret alongside the two MDM settings.
benjamsf
added a commit
to pvarki/docker-rasenmaeher-integration
that referenced
this pull request
Oct 9, 2026
PR pvarki/python-rasenmaeher-api#193, merged up to main.
--find-links is additive, so pip re-resolved against PyPI and took a newer sqlmodel that widens its SQLAlchemy range. SQLAlchemy 2.1 points postgresql:// at psycopg3, which is not installed, so the image crashed on startup with ModuleNotFoundError while the lock said 2.0.52.
Requiring the per-enrolment code changed what a device has to send, but only the csr_utils tests were updated -- the two enrollment tests that expect a success still sent a bare CN and were refused. The code riding into the issued certificate is pinned too, since the signer copies the request's subject. Also stops test_list_as_adm asserting on whichever row came back first: a device planned for MDM carries its code on purpose, so that was a coin flip on what earlier tests had left behind.
15s covers booting the whole app on a developer machine, not on a loaded shared runner.
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.
Description
Device enrolment through an MDM. The workflow
This pr adds the agent path to
POST /enrollment/accept, gated on a service CN inRM_MDM_AGENT_CNS, plusGET /mdm-settingsfor the admin UI.Security fixes carried here:
verify_csrcompared a rendered subject string, so a crafted subject passedsubject, because the SCEP challenge in front of it is not a secret
single UPDATE
User-Facing Changes
Enrol phones through your MDM (part of that capability)
Phones can be enrolled through the MDM a unit already runs, instead of someone
installing a certificate by hand. An admin plans a callsign in Deploy App and
gives the MDM the device name. The phone then gets its certificate during enrolment,
with the key generated on the device.
Anything Else You'd Like to Mention
Two known gaps, neither new here: wrong per-enrolment codes are free and
unlimited (no attempt counter), and the code lands in the issued certificate's
subject because the signer copies the request verbatim.