mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:51:30 +00:00
622cd29f20
Per the cowork/docs-audit-2026-05-05/ end-to-end factuality audit (20 confirmed findings across 76 docs, 7 parallel subagents + audit-of-the-audit). Hot + Warm tier fixes ship here; STALE findings (qa-test-suite.md test-count snapshot) need 'make qa-stats' which is operator-side. BROKEN links repaired (3): - docs/reference/api.md L195: [Quick Start](quickstart.md) → ../getting-started/quickstart.md (404 pre-fix) - docs/reference/api.md L196: [Connector Guide](connectors.md) → connectors/index.md (Phase 4 rename, was 404 pre-fix) - docs/reference/protocols/scep-intune.md L377: [legacy-est-scep.md](legacy-est-scep.md) → scep-server.md (file was deleted in Phase 7 commite9b1510) INCORRECT count claims repaired (12): - api.md L5 + L18-19 + L155: '78 API operations' / '# 78' / 'all 78 documented operations' → re-derive via grep -cE '^\s+operationId:' (actual at HEAD: 144) - architecture.md L66 (Mermaid label) + L502 + L1047 + L1253: '8 always-on + 4 optional loops' / '12-loop topology' → 9 always-on + 5 opt-in loops (14 total). Always-on/opt-in breakdown derived from cmd/server/main.go startup wiring: always-on are agentHealthCheck, crlGeneration, jobProcessor, jobRetry, jobTimeout, notificationProcess, notificationRetry, renewalCheck, shortLivedExpiryCheck (9); opt-in are networkScan, digest, healthCheck, cloudDiscovery, acmeGC (5). Re-derive count via grep -cE '^func \(s \*Scheduler\) [a-zA-Z]+Loop' internal/scheduler/scheduler.go. - configuration.md L31: '12 loops, 8 always-on + 4 opt-in' → '14 loops, 9 always-on + 5 opt-in'. Self-introduced regression from commit3275f9f(2026-05-05). - mcp.md L11 + L65: 'all 78 API endpoints' / '78 available tools' → re-derive via grep -cE 'mcp\.AddTool\(' (actual at HEAD: 87 MCP tools, 144 API operations). - connectors/index.md L111: '9 built-in' issuer connectors → '12 built-in', extending the inline enumeration to include Entrust, GlobalSign, EJBCA (which had been added since the L111 prose was written). Local-CA framing extended to mention tree mode + ADCS sub-CA mode-doc. - connectors/index.md L112: '14 built-in' target connectors → '15 built-in', adding AWS ACM target + Azure Key Vault target (which had been added since the L112 prose was written). - why-certctl.md L37 + the inline list: 'Nine issuer connectors ship today' → 'Twelve issuer connectors', adding AWS ACM PCA, Entrust, GlobalSign, EJBCA to the list and removing the misleading 'EST enrollment' bullet (EST is a protocol surface, not an issuer; clarified in trailing note). - why-certctl.md L66: '13 deployment targets' → '15', adding Kubernetes Secrets, AWS ACM, and Azure KV to the inline list. - why-certctl.md L92: 'supports 9 issuer types' → '12 issuer types'. - quickstart.md L135: '35 demo certificates across 5 issuers' → re-derive cert count via 'grep -oE "mc-[a-z0-9_-]+" migrations/seed_demo.sql | sort -u | wc -l' (actual: 32, matches README L86; quickstart was off-by-3). - quickstart.md L452 (Demo Data Reference table): Certificates '35' → '32' (matches the cert count from seed_demo.sql). Verification: - grep confirms no remaining stale refs across the touched files (8 files, 31 insertions / 28 deletions). - All 24 ci-guards/*.sh pass locally. - The audit's STALE findings (S-1, S-2 qa-test-suite.md Bundle-P snapshot) are operator-side: run 'make qa-stats' to refresh the Test Suite Health table. Companion: cowork/docs-audit-2026-05-05/RESULTS.md captures the full audit with subagent false positives and missed findings called out.
388 lines
24 KiB
Markdown
388 lines
24 KiB
Markdown
# Microsoft Intune SCEP enrollment via certctl
|
|
|
|
> Last reviewed: 2026-05-05
|
|
|
|
> **Status (this document):** Phase 11 of the SCEP RFC 8894 + Intune master
|
|
> bundle. The behavior described here is shipped on `master` and exercised
|
|
> end-to-end by `internal/api/handler/scep_intune_e2e_test.go`. The
|
|
> bundle is V2-free (community edition) — Conditional-Access
|
|
> device-state gating, native Microsoft Graph integration, and
|
|
> per-tenant trust anchors are documented under
|
|
> [Limitations](#limitations) as V3-Pro features.
|
|
|
|
## TL;DR
|
|
|
|
certctl is a **drop-in NDES replacement** for Microsoft Intune SCEP fleets.
|
|
Intune-managed devices keep using the existing Intune Certificate Connector;
|
|
only the SCEP server URL changes. certctl validates the Connector's
|
|
signed challenge using its installation signing cert (no Microsoft API
|
|
calls — the Connector already did that), binds the device claim to the
|
|
inbound CSR, and issues through whichever certctl issuer connector you
|
|
have configured (local CA, Vault, EJBCA, ADCS, etc.).
|
|
|
|
What you get over NDES:
|
|
|
|
- Per-profile SCEP endpoints (`/scep/corp` vs. `/scep/iot` etc.) so a
|
|
single certctl deploy serves multiple device fleets with distinct
|
|
challenge passwords + trust anchors.
|
|
- Audit log entries with the device GUID, claim subject, and CSR
|
|
binding details — much better forensics than NDES + IIS logs.
|
|
- Trust anchor reload via `SIGHUP` (no service restart) when the
|
|
Connector signing cert rotates.
|
|
- A built-in admin GUI tab (Intune Monitoring) showing per-profile
|
|
enrollment counters, trust-anchor expiry countdowns, and the recent
|
|
failures table.
|
|
- Per-device rate limit (sliding window log keyed by Subject + Issuer)
|
|
that catches a compromised Connector signing key issuing many
|
|
different valid challenges for the same device.
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Cloud["Intune cloud<br/>(Microsoft)"]
|
|
Connector["Intune Certificate Connector<br/>(customer infra)"]
|
|
Server["certctl SCEP server<br/>(you)"]
|
|
Issuer["issuer connector<br/>(local CA / Vault / EJBCA / …)"]
|
|
Cloud --> Connector --> Server --> Issuer
|
|
```
|
|
|
|
**certctl replaces NDES, not the Connector.** The Intune Certificate
|
|
Connector is the bridge between the Intune cloud and your on-prem PKI;
|
|
Microsoft installs and maintains it. What you replace is the
|
|
**Network Device Enrollment Service** (NDES) — the SCEP server
|
|
historically deployed on a Windows host, sitting between the Connector
|
|
and an Active Directory Certificate Services CA. certctl sits in
|
|
exactly that slot and speaks SCEP RFC 8894 to the Connector.
|
|
|
|
### What certctl validates per request
|
|
|
|
For every Intune-flavored SCEP request the dispatcher in
|
|
`internal/service/scep.go::dispatchIntuneChallenge` walks the
|
|
following gates in order. A failure on any gate produces a CertRep
|
|
PKIMessage with the documented `pkiStatus`/`failInfo` codes (per RFC
|
|
8894 §3.2.1.4.5) and increments the corresponding metric counter.
|
|
|
|
1. **Shape pre-check** — `looksIntuneShaped(challengePassword)`:
|
|
length > 200 + exactly two dots. False positives are fine; false
|
|
negatives on real Intune challenges would route them to the static
|
|
compare and reject. The pre-check just decides whether to invoke
|
|
the full validator.
|
|
2. **JWS signature** — `intune.ValidateChallenge` re-derives the
|
|
signing input from the raw on-wire bytes (per RFC 7515 §3.1, NOT
|
|
re-base64-encoded segments) and verifies against every cert in the
|
|
trust anchor pool. Supports RS256 and ES256 (both fixed-width
|
|
r||s and ASN.1-DER form). Explicitly rejects `alg=none` and
|
|
HMAC algs.
|
|
3. **Version dispatch** — extracts the `version` claim from the
|
|
payload prelude. v1 (current Connector format, no `version` key)
|
|
routes to `unmarshalChallengeV1`. Future v2 plugs in a sibling
|
|
parser without touching the validator.
|
|
4. **Time bounds** — `now+tolerance ≥ iat AND now-tolerance < exp`.
|
|
The `±tolerance` window is configurable per profile via
|
|
`INTUNE_CLOCK_SKEW_TOLERANCE` (default 60s, covers modest clock
|
|
drift between the Connector host and certctl). Configurable cap on
|
|
top via `INTUNE_CHALLENGE_VALIDITY` (defense-in-depth against a
|
|
Connector that mints long-validity challenges). The validator
|
|
refuses `tolerance ≥ ChallengeValidity` at startup-validation time
|
|
to keep the cap meaningful.
|
|
5. **Audience pin** — `claim.aud == INTUNE_AUDIENCE` (skipped when
|
|
`INTUNE_AUDIENCE` is empty for proxy/load-balancer scenarios).
|
|
6. **CSR binding** — `claim.DeviceMatchesCSR(csr)` checks
|
|
set-equality between the claim's `device_name` / `san_dns` /
|
|
`san_rfc822` / `san_upn` and the CSR's CN + SANs. Set-equality
|
|
means the CSR carries EXACTLY the claim's values, no extras and
|
|
no missing.
|
|
7. **Replay** — `intune.ReplayCache.CheckAndInsert` rejects
|
|
duplicates within the configured TTL. Sized for 100k entries
|
|
(covers a ~25 RPS Intune fleet's steady-state).
|
|
8. **Per-device rate limit** — sliding window log keyed by
|
|
`(claim.Subject, claim.Issuer)`. Catches a compromised Connector
|
|
issuing many DIFFERENT valid challenges for the same device. Default
|
|
3 enrollments per 24h covers legitimate first-cert + recovery +
|
|
post-wipe.
|
|
9. **Optional device-state check** — V3-Pro plug-in seam
|
|
(nil-default no-op). When set, the gate calls Microsoft Graph's
|
|
device-compliance API and short-circuits failing devices with
|
|
FAILURE+BadRequest.
|
|
|
|
A request that passes all nine gates flows to
|
|
`processEnrollment`, which builds the issuance request, calls the
|
|
configured issuer connector, and emits a CertRep PKIMessage with the
|
|
issued cert encrypted to the device's transient signing cert per RFC
|
|
8894 §3.3.2.
|
|
|
|
## Migration from NDES + EJBCA (or NDES + ADCS)
|
|
|
|
The migration plan below is conservative — install certctl alongside
|
|
your existing NDES so you can flip Intune profiles fleet-by-fleet
|
|
without a flag day. Validated against a fresh `docker compose up`
|
|
stack; the docker-compose.test.yml stack does not currently bake
|
|
Intune in (Phase 10.2 ships a hermetic in-process e2e test instead),
|
|
so the production validation step is a manual run-book item.
|
|
|
|
1. **Install certctl alongside existing NDES.** Stand up the certctl
|
|
server on a separate host (or as a Kubernetes deployment) reachable
|
|
from the Connector host. Use the existing operator-run-book in
|
|
`docs/tls.md` for the TLS bootstrap.
|
|
2. **Configure a per-profile SCEP endpoint.** Pick a path id (e.g.
|
|
`corp` — referenced as `<NAME>` below; the value gets uppercased
|
|
for the env-var key and lowercased for the URL path) and set:
|
|
|
|
```
|
|
CERTCTL_SCEP_ENABLED=true
|
|
CERTCTL_SCEP_PROFILES=corp
|
|
CERTCTL_SCEP_PROFILE_<NAME>_ISSUER_ID=iss-local # or your existing issuer
|
|
CERTCTL_SCEP_PROFILE_<NAME>_CHALLENGE_PASSWORD=<random> # Intune still requires this
|
|
CERTCTL_SCEP_PROFILE_<NAME>_RA_CERT_PATH=/etc/certctl/ra-corp.pem
|
|
CERTCTL_SCEP_PROFILE_<NAME>_RA_KEY_PATH=/etc/certctl/ra-corp.key
|
|
```
|
|
|
|
The endpoint will be served at `https://certctl.example.com/scep/corp`
|
|
— the URL path uses the lowercased name and the env-var keys use
|
|
the uppercased form. Concrete env-var name mappings are listed in
|
|
[`features.md`](features.md).
|
|
3. **Extract the Intune Connector's signing cert.** On the Connector
|
|
host (Windows), the Connector's installation creates a self-signed
|
|
cert in the local machine's `Personal` cert store with subject
|
|
`CN=Microsoft Intune Certificate Connector` (path documented by
|
|
Microsoft — see Microsoft Learn link in the
|
|
[Microsoft support statement](#microsoft-support-statement) below).
|
|
Export the public cert (no private key) as a base64 `.cer` file.
|
|
4. **Configure the trust anchor.** Copy the `.cer` to the certctl host
|
|
(or mount via your secret manager) and set:
|
|
|
|
```
|
|
CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_ENABLED=true
|
|
CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_CONNECTOR_CERT_PATH=/etc/certctl/intune-corp.pem
|
|
CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_AUDIENCE=https://certctl.example.com/scep/corp
|
|
CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_CHALLENGE_VALIDITY=60m
|
|
CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_CLOCK_SKEW_TOLERANCE=60s # ±tolerance on iat/exp; raise on poorly-NTP-synced fleets, lower to enforce strict time
|
|
CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_PER_DEVICE_RATE_LIMIT_24H=3
|
|
```
|
|
|
|
Restart certctl. The startup preflight refuses to boot if the
|
|
trust anchor file is missing, unparseable, or contains an expired
|
|
cert — failure is loud at boot rather than silent at request time.
|
|
5. **Configure the issuer connector.** If you're keeping EJBCA,
|
|
point `CERTCTL_SCEP_PROFILE_<NAME>_ISSUER_ID` at your EJBCA issuer
|
|
profile (see `docs/connectors.md`). For a clean cut-over to the
|
|
built-in local CA, follow `docs/tls.md` to bootstrap a sub-CA cert.
|
|
6. **Migrate one Intune SCEP profile to certctl.** In the Intune
|
|
admin center, edit the SCEP profile for a small canary device
|
|
group and update the SCEP server URL to
|
|
`https://certctl.example.com/scep/corp`. Push the profile and
|
|
wait for the canary devices to rotate (24-48h).
|
|
7. **Verify enrollment.** Open the certctl admin GUI's
|
|
[SCEP Intune Monitoring tab](#operational-monitoring) and watch
|
|
the `success` counter tick on the `corp` profile card. The
|
|
`recent failures` table surfaces any rejected enrollments with
|
|
the exact reason (e.g. `signature_invalid`, `claim_mismatch`).
|
|
8. **Roll out the rest of the fleet.** Once the canary is clean,
|
|
migrate the remaining Intune SCEP profiles in batches.
|
|
9. **Decommission NDES.** After all fleets are migrated and a few
|
|
renewal cycles have completed cleanly, take down the NDES role
|
|
and the IIS site. The existing certs continue to chain to your
|
|
issuer; only the enrollment path changes.
|
|
|
|
## Intune SCEP profile fields → certctl behavior
|
|
|
|
The Intune admin center's SCEP profile editor exposes a fixed set of
|
|
fields. The mapping below is what each field controls relative to
|
|
certctl's behavior.
|
|
|
|
| Intune profile field | certctl behavior |
|
|
|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
| Certificate type | Treated as device or user; surfaces in the claim's `subject` field (device GUID vs. user UPN). certctl doesn't gate on type; the issuer's certificate profile decides. |
|
|
| Subject name format | Drives the CSR's CN. The Intune Connector sets `claim.device_name` from this value; certctl's CSR-binding gate enforces equality. |
|
|
| Subject alternative name | Drives the CSR's SAN list. Intune supports DNS / RFC 822 / UPN; certctl's claim binding checks set-equality per dimension. Mismatches surface as `ErrClaimSANDNSMismatch` / `_SANRFC822Mismatch` / `_SANUPNMismatch`. |
|
|
| Certificate validity period | Honored by the issuer connector. certctl caps via the per-profile `CertificateProfile.MaxTTLSeconds`; the smaller of the two wins. |
|
|
| Key storage provider | Device-side concern (the Connector negotiates with the device's TPM / Software KSP). certctl never sees the device's private key — it only signs the CSR. |
|
|
| Key usage / Extended key usage | Honored by the issuer connector via the bound `CertificateProfile.AllowedEKUs`. CSRs requesting an EKU outside the allowed set are rejected by the crypto-policy gate (`ValidateCSRAgainstProfile`). |
|
|
| Hash algorithm | The CSR's signature hash (SHA-256 typical). The SCEP `GetCACaps` advertises SHA-256 + SHA-512; the device picks. |
|
|
| SCEP server URL | The endpoint URL the Connector posts to. Set to `https://certctl.example.com/scep/<profile-name>`. |
|
|
|
|
## Trust anchor extraction
|
|
|
|
The Intune Certificate Connector self-signs an installation cert at
|
|
install time. To configure certctl, extract this cert (PUBLIC ONLY,
|
|
no private key) as PEM:
|
|
|
|
1. On the Connector host (Windows), open `certlm.msc` (Local Machine
|
|
Certificate Manager).
|
|
2. Navigate to `Personal` → `Certificates`. Find the cert with
|
|
subject `CN=Microsoft Intune Certificate Connector`.
|
|
3. Right-click → All Tasks → Export. Choose **No, do not export
|
|
the private key**. Format: **Base-64 encoded X.509 (.CER)**.
|
|
4. Copy the resulting `.cer` file to the certctl host. Rename to
|
|
`.pem` (the bytes are identical; certctl's PEM loader accepts
|
|
either extension).
|
|
5. Set `CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_CONNECTOR_CERT_PATH` to
|
|
the file path.
|
|
6. If you have multiple Connectors in HA, repeat steps 1-3 on each
|
|
and concatenate the PEM blocks into one bundle file.
|
|
|
|
When the operator rotates the Connector signing cert (typically once
|
|
every few years per Microsoft's Connector lifecycle), repeat the
|
|
extraction, overwrite the on-disk file, then send `SIGHUP` to the
|
|
certctl process. The trust holder swaps atomically; bad files (parse
|
|
error, expired cert) keep the OLD pool in place so a half-rotation
|
|
doesn't take Intune enrollment down.
|
|
|
|
## Troubleshooting
|
|
|
|
The dispatcher emits a typed metric label per failure mode plus a
|
|
matching audit-log entry. The table below maps the label to the most
|
|
common root cause and the operator action.
|
|
|
|
| Counter label | Symptom | Root cause + fix |
|
|
|----------------------|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
| `signature_invalid` | Every enrollment from a specific profile failing | Trust anchor mismatch — the Connector's signing cert was rotated and certctl wasn't reloaded. Re-extract the cert ([trust anchor extraction](#trust-anchor-extraction)), overwrite the file, send `SIGHUP`. |
|
|
| `claim_mismatch` | Some enrollments from one Intune SCEP profile failing | The Intune SCEP profile's SAN config doesn't match what the device CSR actually has. Compare the `recent failures` table's claim row to the device's CSR; usually a SAN format mismatch (e.g. claim wants UPN, CSR has DNS). |
|
|
| `expired` | All enrollments failing on a date boundary | Either clock skew between the Connector host and certctl (NTP both ends) OR the Connector's signing cert is past `NotAfter`. The certctl preflight catches an expired trust anchor at boot; check the Monitoring tab's expiry countdown. |
|
|
| `not_yet_valid` | All enrollments failing | Reverse clock skew (certctl's clock is BEHIND the Connector's). Sync via NTP. |
|
|
| `wrong_audience` | All enrollments from a profile failing | `INTUNE_AUDIENCE` doesn't match the URL the Connector is configured to call. Either fix `INTUNE_AUDIENCE` to match the operator URL, or unset it (defense-in-depth then disabled — the claim's exp + sig still gate). |
|
|
| `replay` | Sporadic per-device failures, mostly during retries | The device retried the SAME challenge after the first one failed. The replay cache TTL is `INTUNE_CHALLENGE_VALIDITY` (default 60m). Either widen the device's retry window (Intune-side) or shorten validity. |
|
|
| `rate_limited` | A specific device hitting `429`-equivalent failures | The device exceeded `INTUNE_PER_DEVICE_RATE_LIMIT_24H` (default 3). If legitimate (post-wipe + recovery + first-cert all in 24h), bump the cap. If suspicious, this is the limiter doing its job — investigate the device. |
|
|
| `unknown_version` | Sudden onset of failures across the entire fleet | Microsoft shipped a new Connector version with a `version` claim certctl doesn't understand. Open an issue on the certctl repo with the failing claim payload (anonymized); the parser dispatcher accepts new versions in ~30 LoC. |
|
|
| `malformed` | Sporadic, low-volume | Malformed challenge bytes — almost always a network proxy mangling the request body, or the Connector logging itself out mid-handshake. Capture a packet trace; the Connector should re-emit on the next device retry. |
|
|
| `device_state_failed` | V3-Pro only | The pluggable device-state check rejected the device. The audit-log details carries the reason string from Microsoft Graph. V2 deployments never see this counter tick. |
|
|
|
|
## Operational monitoring (SCEP Administration → Intune Monitoring tab)
|
|
|
|
The admin GUI surface for SCEP lives at `/scep` and is structured as
|
|
three tabs: **Profiles** (default landing — every configured SCEP
|
|
profile, lean cards with always-present fields), **Intune Monitoring**
|
|
(the Intune-specific deep-dive described below), and **Recent Activity**
|
|
(full SCEP audit log filter). Operators monitoring an Intune deployment
|
|
spend most of their time on the Intune Monitoring tab, deep-linkable via
|
|
`/scep?tab=intune` or the legacy alias `/scep/intune`. The Profiles tab
|
|
gives the at-a-glance per-profile health (RA cert expiry, mTLS status,
|
|
Intune enabled/disabled badge, challenge-password-set indicator) and a
|
|
"View Intune details →" link from each Intune-enabled card that switches
|
|
into this tab filtered to that profile.
|
|
|
|
The Intune Monitoring tab shows:
|
|
|
|
- **Per-profile cards** — one card per SCEP profile, with the trust
|
|
anchor expiry countdown badge:
|
|
- `green` ≥ 30 days remaining
|
|
- `amber` 7-30 days remaining (rotate soon)
|
|
- `red` < 7 days remaining
|
|
- `EXPIRED` past `NotAfter`
|
|
- **Live counters** — the per-status enrollment counts polled every
|
|
30s. The order in the grid puts `success` first (vanity) and
|
|
failure modes after.
|
|
- **Recent failures table** — the last 50 audit-log events with
|
|
action `scep_pkcsreq_intune` or `scep_renewalreq_intune`, sorted
|
|
by timestamp descending. Polled every 60s.
|
|
- **Trust anchor reload button** — confirms via modal then issues
|
|
`POST /api/v1/admin/scep/intune/reload-trust` (the SIGHUP-equivalent).
|
|
Bad reloads keep the OLD pool in place; the modal stays open with
|
|
the underlying error so the operator can correct the file and retry.
|
|
|
|
Three admin endpoints back the page:
|
|
|
|
- `GET /api/v1/admin/scep/profiles` — per-profile snapshot for the
|
|
Profiles tab; surfaces RA cert subject + NotAfter + days-to-expiry,
|
|
mTLS sibling-route status + bundle path, challenge-password-set flag,
|
|
and an optional `intune` sub-block for Intune-enabled profiles.
|
|
- `GET /api/v1/admin/scep/intune/stats` — Intune-specific deep-dive
|
|
for the Intune Monitoring tab; per-status counters + trust anchor
|
|
pool details. Backward-compat shape preserved from Phase 9.
|
|
- `POST /api/v1/admin/scep/intune/reload-trust` — SIGHUP-equivalent
|
|
trust anchor reload, body `{"path_id": "<pathID>"}`.
|
|
|
|
All three are M-008 admin-gated. Non-admin Bearer callers get HTTP 403
|
|
+ a clear message; the GUI hides the page entirely for non-admin users
|
|
(UX hint; server-side enforcement is independent).
|
|
|
|
### Recommended alert thresholds
|
|
|
|
The counters are exposed in the GUI as snapshots; if you wrap them
|
|
in a Prometheus exporter (V3-Pro plug-in seam — V2 doesn't ship a
|
|
`/metrics` surface today), reasonable starting thresholds:
|
|
|
|
- `signature_invalid` rate > 0 for > 5 minutes → page on-call. The
|
|
trust anchor is stale; the operator missed a SIGHUP after a
|
|
Connector rotation.
|
|
- `claim_mismatch` rate > 0 sustained > 1 hour → notify (not page).
|
|
An Intune SCEP profile is misconfigured; an admin needs to fix
|
|
the SAN definition or the operator's CertificateProfile.
|
|
- `replay` rate climbing → notify. Either an aggressive retry policy
|
|
on the device side OR active replay attempts. Cross-reference
|
|
source IPs in the audit log.
|
|
- `rate_limited` for a single device > 1 per hour → notify. Either
|
|
legitimate enrollment storm (post-wipe scenarios) or a compromised
|
|
Connector signing key.
|
|
- Trust anchor `days_to_expiry` < 30 on any profile → notify; rotate
|
|
the Connector's signing cert before the cliff.
|
|
|
|
## Limitations
|
|
|
|
This bundle is V2-free. The following capabilities are deferred to
|
|
V3-Pro:
|
|
|
|
- **Native Microsoft Graph integration.** certctl validates the
|
|
Connector's signed challenge but doesn't call Microsoft's API
|
|
directly — the Connector already did that. V3-Pro could ship a
|
|
Graph client that pulls device-compliance state in addition to
|
|
the challenge claim.
|
|
- **Conditional Access device-state gating.** The dispatcher exposes
|
|
a nil-default `DeviceStateCheck` hook. V3-Pro plugs in a Microsoft
|
|
Graph device-compliance lookup before issuance; failing devices
|
|
exit with a typed `device_state_failed` failInfo.
|
|
- **Per-tenant trust anchors.** V2 has one trust anchor pool per
|
|
SCEP profile; V3-Pro could support per-AAD-tenant anchor scoping
|
|
for MSPs running shared certctl deployments across customers.
|
|
- **OCSP stapling at SCEP-response time.** The CertRep doesn't carry
|
|
a stapled OCSP response today; certificate validators look up OCSP
|
|
via the `id-pkix-ocsp` extension on the issued cert. V3-Pro could
|
|
staple inline.
|
|
- **Auto-discovery of the Connector signing cert.** V2 requires the
|
|
operator to extract the cert manually and configure the path.
|
|
V3-Pro could pull from a Microsoft-published endpoint (with the
|
|
appropriate trust constraints).
|
|
|
|
These deferrals are deliberate, not oversights. The V2 surface
|
|
covers every operationally-required path for a single-tenant
|
|
enterprise replacing NDES; V3-Pro adds the multi-tenant + native-API
|
|
features procurement teams sometimes ask for.
|
|
|
|
## Microsoft support statement
|
|
|
|
Microsoft documents the Intune Certificate Connector as
|
|
**RFC-8894-compliant** and supports its use against any RFC 8894
|
|
SCEP server. The relevant Microsoft Learn pages:
|
|
|
|
- [Intune Certificate Connector overview](https://learn.microsoft.com/en-us/mem/intune/protect/certificate-connector-overview) —
|
|
documents the Connector's architecture and explicitly notes it
|
|
speaks RFC-8894-compliant SCEP.
|
|
- [Use SCEP certificate profiles in Intune](https://learn.microsoft.com/en-us/mem/intune/protect/certificates-scep-configure) —
|
|
the operator-facing setup guide, with the SCEP server URL field
|
|
the migration playbook above edits.
|
|
- [Validate setup of Intune Certificate Connector](https://learn.microsoft.com/en-us/mem/intune/protect/certificate-connector-install) —
|
|
the install-validation checklist; useful when troubleshooting
|
|
Connector-side failures vs. certctl-side failures.
|
|
|
|
certctl's role per Microsoft's framing: a third-party SCEP server
|
|
that the Connector posts to. Microsoft supports this topology; only
|
|
certctl's own RFC 8894 implementation is in scope for certctl
|
|
support. The end-to-end Connector → certctl → issuer flow is
|
|
exercised in `internal/api/handler/scep_intune_e2e_test.go` and
|
|
the golden-file fixtures in `internal/scep/intune/testdata/`.
|
|
|
|
## Related docs
|
|
|
|
- [`scep-server.md`](scep-server.md) — the per-profile SCEP setup
|
|
guide + RFC 8894 reference + mTLS sibling route. Read this first
|
|
if you're not already running certctl SCEP for non-Intune fleets.
|
|
- [`architecture.md`](architecture.md) — overall control-plane
|
|
architecture; Security Model section calls out the Intune trust
|
|
anchor as a sensitive operator-configured surface.
|
|
- [`features.md`](features.md) — every `CERTCTL_*` env var,
|
|
including the per-profile `CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_*`
|
|
family.
|
|
- [`tls.md`](tls.md) — TLS bootstrap for the certctl control plane;
|
|
prerequisite for any production deploy.
|