Skip to content

[WIP] certificates: Allow LDAP enrollment#1401

Draft
denisonbarbosa wants to merge 24 commits into
mainfrom
ldap-cert-enrollment
Draft

[WIP] certificates: Allow LDAP enrollment#1401
denisonbarbosa wants to merge 24 commits into
mainfrom
ldap-cert-enrollment

Conversation

@denisonbarbosa

Copy link
Copy Markdown
Member

Allow certificate enrollment through LDAP protocol.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new native (Go) certificate auto-enrollment path that discovers AD CS enrollment data via LDAP and submits CSRs via MS-ICPR (RPC), while keeping the existing CEPCES/Python path for backward compatibility and wiring enrollment-method selection through daemon/service configuration and packaging/docs.

Changes:

  • Add LDAP discovery, Kerberos GSSAPI bind support, MS-ICPR CSR submission, and on-disk enrollment state/trust-store management in the certificate policy manager.
  • Add configuration plumbing (certificate_enrollment) across daemon/service/policy manager, plus updated tests and integration fixtures.
  • Update docs and Debian packaging to reflect the new enrollment method options and remove legacy script-dump flows.

Reviewed changes

Copilot reviewed 64 out of 99 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
internal/policies/manager.go Plumbs certificate manager injection and enrollment method into policy manager construction.
internal/policies/manager_test.go Updates policy-manager tests to use the new certificate manager wiring and LDAP enrollment mocks.
internal/policies/certificate/trust.go Adds root CA installation/symlink creation and trust-store update helpers.
internal/policies/certificate/state.go Adds JSON state persistence for enrollment (replacing Samba TDB cache).
internal/policies/certificate/ldap.go Implements LDAP discovery and Kerberos (GSSAPI) bind support and ccache lookup.
internal/policies/certificate/ldap_test.go Adds unit tests for LDAP discovery/template lookup helpers.
internal/policies/certificate/gssapi.go Adds a Go LDAP GSSAPI client implementation for Kerberos binds.
internal/policies/certificate/gssapi_test.go Adds unit tests for GSSAPI token unwrap/wrap behavior and RRC handling.
internal/policies/certificate/enroll.go Implements MS-ICPR CSR submission and key/CSR generation helpers.
internal/policies/certificate/enroll_test.go Adds tests for RPC credential creation from Kerberos ccaches.
internal/policies/certificate/certificate.go Introduces dual enrollment methods (ldap/cepces) and LDAP enrollment flow (state + cert/key writing).
internal/policies/certificate/certificate_test.go Reworks certificate policy tests to exercise LDAP enrollment path and state cleanup.
internal/policies/certificate/testdata/TestApplyPolicy/golden/computer,_no_entries,_samba_cache_present Removes golden output tied to legacy Python autoenroll script behavior.
internal/policies/certificate/testdata/TestApplyPolicy/golden/computer,_configured_to_unenroll Removes golden output tied to legacy Python autoenroll script behavior.
internal/policies/certificate/testdata/TestApplyPolicy/golden/computer,_configured_to_enroll,_advanced_configuration Removes golden output tied to legacy Python autoenroll script behavior.
internal/policies/certificate/testdata/TestApplyPolicy/golden/computer,_configured_to_enroll Removes golden output tied to legacy Python autoenroll script behavior.
internal/consts/consts.go Adds constants for enrollment method selection and defaults.
internal/adsysservice/policy.go Changes CertAutoEnrollScript RPC response to informational text (no longer returns embedded script).
internal/adsysservice/adsysservice.go Adds service option to pass certificate_enrollment into the policy manager.
internal/ad/ad.go Adds test-only env var to disable Kerberos in integration environments without a KDC.
cmd/adsysd/daemon/daemon.go Adds CLI/config flag plumbing for certificate_enrollment.
cmd/adsysd/client/policy.go Removes CLI command that dumped the legacy cert-autoenroll script.
cmd/adsys-certsubmit/main.go Adds a new helper binary intended for certmonger-style operations (SUBMIT/IDENTIFY/GET-SUPPORTED-TEMPLATES).
cmd/adsysd/integration_tests/adsysctl_policy_test.go Splits certmonger/cepces missing-binary scenarios and removes script-dump tests; improves NSS wrapper handling.
cmd/adsysd/integration_tests/adsys_test.go Ensures integration tests run with Kerberos disabled and augments passwd/group for container mocks.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/systemd/system/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/users/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/.ready Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/startup Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/empty-subfolder/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/final-machine-script.sh Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/other-script-user-logon Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/otherfolder/script-user-logoff Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/script-machine-shutdown Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/script-machine-startup Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/script-user-logon Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/subfolder/other-script Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/unreferenced-data Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/run/machine/scripts/scripts/unreferenced-script Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/sudoers.d/99-adsys-privilege-enforcement Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/polkit-1/localauthority.conf.d/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/polkit-1/rules.d/00-adsys-privilege-enforcement.rules Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/apparmor.d/adsys/machine/usr.bin.bar Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/apparmor.d/adsys/machine/usr.bin.foo Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/apparmor.d/adsys/machine/nested/usr.bin.baz Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/apparmor.d/adsys/users/adsystestuser@example.com Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/dconf/profile/gdm Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/dconf/db/gdm.d/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/dconf/db/gdm.d/locks/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/dconf/db/machine.d/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/dconf/db/machine.d/locks/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/lib/private Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_certmonger_is_not_available/lib/samba/cert_gpo_state_HOST.tdb Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/systemd/system/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/users/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/.ready Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/startup Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/empty-subfolder/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/final-machine-script.sh Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/other-script-user-logon Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/otherfolder/script-user-logoff Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/script-machine-shutdown Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/script-machine-startup Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/script-user-logon Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/subfolder/other-script Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/unreferenced-data Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/run/machine/scripts/scripts/unreferenced-script Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/sudoers.d/99-adsys-privilege-enforcement Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/polkit-1/localauthority.conf.d/.empty Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/polkit-1/rules.d/00-adsys-privilege-enforcement.rules Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/apparmor.d/adsys/machine/usr.bin.bar Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/apparmor.d/adsys/machine/usr.bin.foo Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/apparmor.d/adsys/machine/nested/usr.bin.baz Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/apparmor.d/adsys/users/adsystestuser@example.com Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/dconf/profile/gdm Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/dconf/db/gdm.d/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/dconf/db/gdm.d/locks/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/dconf/db/machine.d/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/dconf/db/machine.d/locks/adsys Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/lib/private Updates golden fixtures for integration test scenario.
cmd/adsysd/integration_tests/testdata/TestPolicyUpdate/golden/does_not_error_when_cepces_is_not_available/lib/samba/cert_gpo_state_HOST.tdb Updates golden fixtures for integration test scenario.
e2e/cmd/run_tests/01_provision_client/main.go Drops python3-cepces install in e2e provisioning.
docs/reference/external-links.md Removes CEP/CES external links section and normalizes formatting.
docs/index.md Adjusts formatting; still references CEP/CES link anchor (now removed elsewhere).
docs/how-to/certificates/setup.md Updates package requirements and introduces method selection documentation.
docs/how-to/certificates/configure.md Documents certificate_enrollment config and updates helper-location example.
docs/how-to/certificates/index.md Removes CEP/CES mention from external resources list.
docs/how-to/certificates/troubleshoot.md Updates troubleshooting guidance toward native LDAP/RPC flow and state files.
docs/explanation/certificates.md Adds method overview/configuration details for LDAP vs CEPCES enrollment.
debian/rules Removes vendored Python install step and adds (currently empty) test override target.
debian/control Moves Python/Samba deps to Recommends and adjusts runtime deps.
debian/adsys.postinst Creates default /etc/adsys.yaml for new installs setting LDAP enrollment.
debian/adsys.install Stops installing legacy Python assets directory.
debian/adsys.apport Removes python3-samba from apport related packages list.
debian/copyright Removes legacy vendored Samba Python copyright stanza.
debian/changelog Adds a test changelog entry for certificate changes.
conf.example/adsys.yaml Documents the new certificate_enrollment configuration key.
go.mod Adds new Go dependencies for LDAP/Kerberos/MSRPC.
go.sum Records new module checksums for added dependencies.
Comments suppressed due to low confidence (1)

debian/rules:88

  • An empty override_dh_auto_test target disables the default test execution in the Debian build. If the intent is not to skip unit tests, call dh_auto_test here (integration tests are already skipped via ADSYS_SKIP_INTEGRATION_TESTS).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/policies/certificate/ldap.go Outdated
Comment thread internal/policies/certificate/ldap.go
Comment thread internal/policies/certificate/trust.go Outdated
Comment thread internal/policies/certificate/state.go
Comment thread internal/policies/certificate/certificate.go Outdated
Comment thread docs/index.md Outdated
Comment thread internal/policies/certificate/certificate_test.go
Comment thread docs/explanation/certificates.md Outdated
Comment thread internal/policies/certificate/certificate_test.go
Comment thread internal/policies/certificate/ldap_test.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 65 out of 99 changed files in this pull request and generated 14 comments.

Comment thread internal/policies/manager.go
Comment thread internal/policies/manager.go
Comment thread internal/policies/certificate/certificate.go
Comment thread internal/policies/certificate/certificate.go
Comment thread internal/policies/certificate/trust.go Outdated
Comment thread internal/adsysservice/policy.go
Comment thread internal/ad/ad.go
Comment thread cmd/adsys-certsubmit/main.go Outdated
Comment thread cmd/adsys-certsubmit/main.go Outdated
Comment thread internal/policies/certificate/certificate.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 66 out of 100 changed files in this pull request and generated 4 comments.

Comment thread debian/rules
Comment thread internal/policies/certificate/certificate.go
Comment thread internal/policies/certificate/certificate.go
Comment thread internal/ad/ad.go Outdated
@denisonbarbosa

Copy link
Copy Markdown
Member Author

Pushed follow-up commits addressing the review threads (now resolved). Summary of what changed:

certificate: harden LDAP/TLS and trust store (updated)

  • LDAP MITM / auth-only SASL: StartTLS verifies the DC certificate against the system store plus adsys-managed CAs before the GSSAPI bind, and the configured global trust dir is now honored.
  • CA validation comment made accurate: a discovered self-signed AD CS root can only be sanity-checked (validity window); its trust rests on the authenticated StartTLS+Kerberos channel it was discovered through (TOFU, as Windows autoenrollment trusts the directory). Non-self-signed CAs must chain to an already-trusted anchor.
  • Issued cert is rejected if expired/not-yet-valid; the RPC certificate response is size-capped; key/cert are written via an O_EXCL temp file + rename.

gate Kerberos bypass behind testing.Testing() — the ADSYS_TESTS_WITHOUT_KERBEROS escape hatch can no longer take effect in production builds.

fix orphaned certs, unreachable skip path, state comment — graceful skip path reachable; orphan cleanup; stale comments corrected.

validate enrollment config, CSR size limit, CEPCES packaging — enrollment method validated/normalized across all layers; CSR size checked before the full read; vendored Python reinstalled; adsys-certsubmit accepts the CA via certmonger env vars.

harden LDAP enrollment robustness and renew certs before expiry (new)

  • LDAP-issued certs are re-enrolled on policy refresh when missing or within 30 days of expiry (this path is not certmonger-tracked); docs updated to match.
  • A CA whose root fails to install/verify is skipped (prior state preserved); the system trust store is rebuilt after both installs and orphan removals; a failed renewal retains the still-valid certificate instead of letting orphan cleanup delete it (covered by a new regression test).

Intentional/by design: the code-level default stays cepces (new installs select ldap via postinst); issued-cert checks do not enforce subject/SAN/EKU/chain (AD CS templates legitimately control these and the issuer may be a subordinate CA); renewal is handled in-daemon rather than via certmonger.

@denisonbarbosa
denisonbarbosa force-pushed the ldap-cert-enrollment branch 2 times, most recently from 2b31c7e to f1bc708 Compare June 26, 2026 21:43
denisonbarbosa and others added 7 commits June 26, 2026 17:46
The native enrollment path has to talk to Active Directory's LDAP
directory as the machine account, but go-ldap can only drive a
GSSAPIClient and ships no Kerberos implementation of its own. Bridging
the two lets us reuse the machine credentials already present in the
host's credential cache instead of provisioning or storing a separate
secret.

The adapter performs the GSSAPI/SASL bind with the machine credentials,
and LDAP discovery upgrades the connection with StartTLS before the bind
so directory responses used for enrollment are protected in transit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The LDAP enrollment method must learn which enrollment services and
certificate templates are available without shelling out to the
Samba-based Python helper. Querying the directory directly makes
discovery a first-class, testable part of the daemon rather than a side
effect of an external process.

The directory client sits behind an interface so tests can inject
responses without a live AD, and connections reuse the machine's
Kerberos cache so no extra credentials are needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Dropping the Samba/CEPCES stack also removes the bookkeeping it provided:
nothing records what has already been enrolled, and the CAs that AD
advertises are not trusted by the system. Left unaddressed, every policy
refresh would request brand new certificates and the issued certificates
would fail to validate.

Persisting enrollment state to disk lets repeated policy applications
reuse the existing key and certificate pairs instead of re-issuing them,
and installing the root CAs discovered over LDAP into the system trust
store means both the issued certificates and the servers presenting them
verify correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Requesting the certificate is the last step that still required cepces.
Submitting the CSR straight to AD CS over the MS-ICPR RPC interface lets
the entire enrollment run natively, authenticated with the machine's
Kerberos credentials and with no Python runtime on the host.

Submission is kept behind an interface so the network call stays out of
unit tests, and the pending (manual-approval) result is handled
explicitly rather than assuming the CA issues immediately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
certmonger drives certificate renewal through an external submit helper,
a role previously filled by cepces-submit. Providing a helper that speaks
MS-ICPR directly keeps certmonger-tracked certificates renewable under
the LDAP method without reintroducing a dependency on cepces.

It implements the submit, identify and template-enumeration operations
certmonger invokes and authenticates with the credentials already
referenced by KRB5CCNAME.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The new LDAP path and the legacy CEPCES path have to coexist so that
upgrading does not break deployments that already rely on cepces.
Introducing explicit configuration values to select between them, and
defaulting to CEPCES, keeps existing installations on their proven path
while allowing new ones to opt into the native method.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With discovery, trust, state persistence and CSR submission in place, the
manager needs to decide which mechanism to run for a given machine.
Dispatching on the configured method lets the daemon perform the whole
LDAP enrollment in-process while still delegating to the embedded Python
script for the CEPCES path.

Threading the choice through the daemon flag, the service option and the
manager options also makes the behaviour injectable, so the full LDAP
flow can be exercised in tests without a live directory. The legacy
cert-autoenroll debug script dump remains available for CEPCES support.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@denisonbarbosa
denisonbarbosa force-pushed the ldap-cert-enrollment branch 2 times, most recently from 772d307 to d3ffabf Compare June 26, 2026 21:54
denisonbarbosa and others added 3 commits July 6, 2026 07:24
Upgrades must not silently change enrollment behaviour under existing
deployments, so the compiled-in default stays cepces. A brand new
install has no such history to preserve and is better served by the
native LDAP/RPC path, which needs no external Python/CEPCES tooling.
Seed /etc/adsys.yaml with the ldap method only when postinst sees no
prior version, leaving upgrades on their existing default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With new installs defaulting to native LDAP enrollment, the Python
CEPCES stack is now only the optional legacy fallback rather than part
of the primary path. Promote python3-cepces from Suggests to Recommends
so that fallback still works out of the box yet can be dropped with
--no-install-recommends, and stop listing python3-samba in the apport
hook since it is no longer relevant to diagnosing the default path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The documentation still presented enrollment as a CEP/CES-only feature,
which no longer matches the default behaviour for new installations.
Explaining that two methods exist, how to choose between them, and what
the native LDAP flow actually does lets administrators configure and
troubleshoot the method their system is running.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@denisonbarbosa
denisonbarbosa force-pushed the ldap-cert-enrollment branch 2 times, most recently from a292570 to cf28df7 Compare July 6, 2026 13:07
denisonbarbosa and others added 14 commits July 15, 2026 07:32
The adsysd integration tests run the daemon against a guest-only smbd
with no real KDC, relying on libsmbclient falling back from kerberos to
anonymous authentication. Samba >= 4.23 dropped that implicit fallback,
so the SMB fetch fails with EINVAL on current systems. Gating a test-only
ADSYS_TESTS_WITHOUT_KERBEROS variable in ad.New() lets the daemon skip
kerberos under the harness; it must never be set in production.

The mock D-Bus/polkit/systemd containers also resolve the connecting user
through the bind-mounted /etc/passwd and /etc/group. When the suite runs
as a user only known through NSS (e.g. an LDAP/SSSD user absent from the
local files), the container cannot resolve the UID and resets the
connection, so the harness now generates augmented passwd/group databases
that include the current user before mounting them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Pin LDAP StartTLS to verify against system trust store plus
  adsys-managed CAs, preventing MITM on first enrollment when the
  AD root CA is not yet in the system trust store.
- Validate discovered CA certificates (self-signed root or chains to
  trusted anchor, not expired) before installing them into the system
  trust store.
- Use atomic symlink creation (temp + rename) to eliminate TOCTOU
  race in trust store symlink replacement.
- Verify issued certificate public key matches the generated private
  key before writing to disk.
- Write private keys and certificates atomically via safeWriteFile.
- Clarify GSSAPI SASL auth-only selection rationale in comments.

Addresses Copilot review threads on ldap.go:64, trust.go:69 and
security findings on unverified CA installation and symlink TOCTOU.
…ment

- Add cleanupOrphanedCerts() to remove cert/key files and trust store
  symlinks from previous enrollment state that are no longer present in
  AD, preventing orphaned trust entries from accumulating.
- Fix ldapPolicyAllowsEnrollment to return (false, nil) instead of
  (false, error) when no enabled LDAP endpoint is found, making the
  'skip' path in applyPolicyLDAP reachable instead of turning a skip
  condition into a hard policy error.
- Fix state.go Nickname comment to reflect it's a sanitized on-disk
  identifier, not a certmonger tracking ID.
- Update tests to match new behavior.

Addresses Copilot review threads on certificate.go:303, certificate.go:540
and state.go:34.
- Validate certificate enrollment method in adsysservice options,
  rejecting typos and unknown values early instead of silently
  falling back to the legacy path.
- Add 64KB size limit on CSR data in adsys-certsubmit to prevent
  memory exhaustion DoS.
- Update CertAutoEnrollScript RPC message to clarify the script is
  deprecated rather than claiming no external script exists.

Addresses Copilot review threads on manager.go:264, adsys-certsubmit
and policy.go:171.
… expiry

Follow-ups to the native LDAP enrollment security review:

- Re-enroll a certificate on policy refresh when it is missing or within
  30 days of expiry (certNeedsRenewal). Issued certs are not tracked by
  certmonger, and an existing cert was previously reused indefinitely as
  long as its file existed, so it could silently expire.
- Skip a CA when its root certificate fails to install or verify,
  preserving any previously-enrolled state so a transient failure does not
  orphan still-valid certificates, instead of issuing certs that cannot
  validate without the root in the trust store.
- Rebuild the system trust store after BOTH installing new roots and
  removing orphaned ones, so removed CAs are purged from the consolidated
  bundle in the same pass.
- Log orphan-removal failures instead of silently dropping them.
- Document that LDAP-issued certificates are renewed before expiry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ers expect

Domain-joined Ubuntu machines are configured by SSSD, whose krb5.conf
usually does not list KDCs because SSSD locates them through DNS SRV
records. The LDAP bind papered over this by loading /etc/krb5.conf and
unconditionally forcing dns_lookup_kdc on, which silently overrode an
administrator's deliberate configuration, and the MS-ICPR submission path
did no KDC discovery at all — so certificate issuance failed on an
otherwise healthy domain member.

Derive the Kerberos configuration once for both paths: keep whatever the
system krb5.conf provides, enable DNS-based KDC discovery only when the
target realm has no KDC configured, and refuse with an explanatory error
when discovery was explicitly turned off but no KDC exists to fall back
on. Resolve the service realm from the target host so cross-realm CA
deployments are honoured, and request the exact host SPN rather than
letting a cached same-host ticket for another service class be reused,
which AD rejects.

Finally, translate the opaque "invalid checksum" RPC rejection into
guidance about the SPN registration, duplication and realm-mapping
mistakes that actually cause it, so operators can fix the environment
instead of guessing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ding

Microsoft is progressively turning on LDAP channel binding enforcement
(LdapEnforceChannelBinding) on domain controllers, and hardened or newer
DCs already refuse a GSSAPI bind that carries no channel binding token,
answering with LDAP result 49 / data 80090346 (SEC_E_BAD_BINDINGS). Our
StartTLS-then-GSSAPI bind sent none, so certificate enrollment could not
even authenticate against those DCs.

Derive the RFC 5929 tls-server-end-point token from the StartTLS server
certificate and carry it in the GSS-API authenticator checksum as RFC
4121 requires, so the bind satisfies a channel-binding-enforcing DC. The
token is advisory for DCs that do not enforce it — they ignore it — so
sending it unconditionally is safe and needs no probing or configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lled CA

Native LDAP enrollment must reach the domain controller over StartTLS to
discover the enterprise CA and templates, yet on a freshly joined machine
that CA is not in any trust store — adsys installs it only later in the
same run. Verifying the DC certificate chain strictly made the very first
enrollment unsolvable: the client could not fetch the CA because it did
not yet trust the certificate that CA had signed.

Break the deadlock the way Windows autoenrollment does — by trusting the
directory rather than a pre-provisioned certificate. On a bootstrap
connection, tolerate only an unknown-authority chain error and anchor
trust on the mutually authenticated Kerberos GSSAPI bind (with TLS
channel binding) performed immediately after StartTLS. Every other
failure — expired, not-yet-valid, wrong hostname — stays fatal, and once
adsys installs the CA later in the run, all subsequent refreshes verify
the full chain strictly.

Hostname checking is also split out from chain building so it can fall
back to the Subject Common Name: domain controller certificates from
legacy templates often carry no Subject Alternative Name, which
crypto/x509 has refused to match since Go 1.15 and which would otherwise
break StartTLS even against a trusted DC. Finally, expose the strict
(non-bootstrap) connector as NewKerberosLDAPConnector so other AD-facing
managers can reuse the same authenticated LDAP path without inheriting
the first-run trust exception.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
go.mod now sets the language version to 1.25.0, so building the package
with the previous golang-go 1.24 build-dependency floor would select a
toolchain older than the source declares it needs. Raise the floor to
1.25 to keep the Debian build in step with the module's stated minimum.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The adsys postinst runs pam-auth-update as part of `configure`, which can
open an interactive prompt. Under a non-interactive install (unattended
upgrades, image builds, CI) that prompt has no controlling terminal and
stalls or fails the whole package configuration. Force
DEBIAN_FRONTEND=noninteractive for the invocation so PAM is updated with
its default answers and the install always completes unattended.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native LDAP enrollment method records issued certificates in adsys'
own JSON state rather than certmonger, which leaves them invisible to
getcert and effectively unmanageable: the troubleshooting guide even
resorted to telling administrators to delete state files by hand,
inviting drift between what is on disk and what adsys believes.

Add a manager-level API (list, status, renew, remove, verify, CA
discovery and template lookup) that works from that state so the daemon
and CLI can offer supported tooling. Every entry point is gated on the
ldap method so cepces deployments keep deferring to certmonger/getcert.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Machine private keys live in a root-only directory and the daemon is the
only privileged component, so certificate management has to run server
side. Surface the new manager API through the existing service so
unprivileged clients can drive it over the daemon socket, the same way
policy commands are streamed.

Reads are allowed to any caller while renewals and removals require the
policy-update authorization, and the "not ldap" sentinel is translated
into an actionable error so cepces hosts are pointed at getcert.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Administrators need a getcert-equivalent workflow for adsys-managed
certificates instead of hand-editing /var/lib/adsys. Add the certificate
command group (list, status, renew, remove, verify, cas, templates) with
optional JSON output so results can feed automation.

status maps certificate health onto a process exit code so it can back
monitoring checks; honouring that meant teaching the top-level runner to
surface an exit code carried on the returned error, without logging it as
a failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the "remove the state file by hand" workaround, which risked
leaving disk and adsys state out of sync, with guidance built around the
new adsysctl certificate commands, and add a dedicated how-to so the
supported inspect/renew/remove workflow is discoverable rather than
buried in troubleshooting.

Also refresh the generated daemon CLI reference, which predated the
existing --certificate-enrollment and --gpo-list-timeout flags, so the
documented options match the shipped binary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants