Connects FleetDM to an OpenDefence deployment's device enrolment.
The deployment knows no MDM. It answers standard SCEP (RFC 8894) at https://<domain>/scep,
asks for a challenge that is new every time an admin opens enrolment in Deploy App, and makes the
certificate's common name the device's callsign. Everything that knows Fleet lives here: the
certificate authority, the certificate template, the apps, the device policy, the launcher icon,
and naming the phones.
Run it from an operator's machine, never inside the deployment: it holds a Fleet API token, and something that can reach every device a unit owns has no business running next to an internet facing service.
Two Fleet teams: phones enrol into a preparation team and are named there, then move to the team that gets the certificate. Naming first means the first certificate request already carries the callsign.
Once per deployment, and whenever the template changes:
odmdm-fleet apply --team "Deploy App" --prep-team Preparation
Enrol a phone into the preparation team, then name it:
odmdm-fleet plan OTTER4
plannames the one phone in the preparation team that has no name yet. With two waiting it names neither, because Fleet shows identical phones as identical rows and nothing tells you which is the one in your hand. Enrol, plan, next phone.Open enrolment in Deploy App (User management → MDM) and give Fleet the new challenge:
ODMDM_FLEET_CHALLENGE=<challenge from the MDM page> odmdm-fleet apply --team "Deploy App"
In Fleet, transfer the named phones to the
Deploy Appteam. Each gets its certificate 7 to 20 minutes later and greets its callsign in Deploy App.
Close enrolment when the last phone is in. The next opening makes a new challenge, so step 3 comes again before the next batch.
odmdm-fleet apply # state what devices need; --force-policy after one joins
odmdm-fleet apply --dry-run # validate the template without touching Fleet
odmdm-fleet plan CALLSIGN # name the one phone waiting to be named
odmdm-fleet reconcile # ask again for phones named after they enrolled; run on a timerreconcile is for a single-team setup, where a phone enrols straight into the certificate team
before anyone has named it. That first request fails, Fleet gives up after three, and
reconcile asks again once the phone has a name.
All configuration is via environment variables:
ODMDM_FLEET_URL- Base URL of Fleet's API.
ODMDM_FLEET_TOKEN_FILE- A file holding the Fleet API token. Changing the certificate authority's challenge needs a global admin token, because Fleet keeps certificate authorities outside teams.
ODMDM_FLEET_TEAM- The team whose phones get the certificate. Default
Deploy App. ODMDM_FLEET_PREP_TEAM- The team phones enrol into and are named in. Empty for a single team.
ODMDM_FLEET_TEMPLATE- The document saying what to install, see below.
ODMDM_FLEET_DOMAIN- The deployment's DNS name.
ODMDM_FLEET_CHALLENGE- The challenge on Deploy App's MDM page. New for every enrolment window.
ODMDM_FLEET_KEY_ALIAS- The certificate template's name, which becomes the Android keystore alias. Default
rmscep, what the first deployments were set up with. Never change it for a deployment: a second key will not install over an existing alias.
What to install is a property of the deployment, so it is a document rather than code: the apps,
their managed configuration, the Android Management API device policy, a launcher link, and the
certificate template. {domain}, {mtls_url} and {key_alias} are filled in. Keys
beginning with _ are comments and never reach Fleet.
The certificate subject is the contract with the deployment. Its common name becomes the callsign: 3 to 32 letters, digits or hyphens, no hyphen at either end. For named phones:
CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME_LOCAL_PART
Three facts about managed Android pull against each other. Each was learned by getting it wrong.
- Apps install at enrolment and at no other time. The team must carry them before a phone joins. So both teams carry the same launcher package: a transfer to a team with another package uninstalls the launcher and never installs the other.
- A profile is delivered when the profile changes, not when a phone arrives. One uploaded
before a phone joined is never sent to it, so a first enrolment needs
--force-policy. - A policy rewritten while apps are installing stops them installing. Leave the phones alone until the apps have arrived.
- A launcher web app's URL can never change. Google publishes it permanently and Fleet cannot delete it. The title carries the deployment's name, so a moved deployment publishes its own launcher and leaves the old one orphaned. Google refuses a title of 30 characters or more.
- A web app's display name is unique within a team (409). A title in a team's setup experience has to be removed there before it can be removed from the team.
- A returning phone keeps its old team and its old name. Transfer it to the preparation team and name it again, or it asks for a name that is already taken.
- Renaming a phone that has its certificate gives it a second callsign. Revoke the old one in Deploy App.
- A refusal shows only as
SCEP enrollment failed ... failInfo: badMessageCheckon the phone's certificate. Usually a bad or taken name, or Fleet still has the previous window's challenge. Fix it, then transfer the phone out and back.
The production target runs the CLI:
docker build --ssh default --target production -t odmdm-fleet:latest . docker run --rm -e ODMDM_FLEET_URL -e ODMDM_FLEET_TOKEN_FILE=/run/token \ -v "$PWD/fleet.token:/run/token:ro" -v "$PWD/template.json:/run/template.json:ro" \ -e ODMDM_FLEET_TEMPLATE=/run/template.json -e ODMDM_FLEET_DOMAIN \ odmdm-fleet:latest odmdm-fleet apply
For the test suite in docker use the tox target, and for a development shell devel_shell.
install uv: https://docs.astral.sh/uv/getting-started/installation/
Install project deps and pre-commit hooks:
uv sync uv run prek install uv run prek run --all-files
Run the tests:
uv run pytest
The .pre-commit-config.yaml is generated by config-ci-library (common + python components), do
not hand edit the autogenerated section.
Versioning is handled with bump-my-version: bump-my-version bump <patch/minor/major>.