Repository navigation
Bind deployed executors before requiring client certificates - #428
Merged
Merged
Conversation
With tls.require_client_cert on, the dispatcher admits an executor only over the certificate bound to its ID in executor_enrollments. Executors deployed with generate-certs.sh and deploy-certs.yml never got a binding, so turning the requirement on disconnected the whole fleet. Add `debuglet-dispatcher -bind-executor ID -bind-certificate PATH`, which verifies an administrator-issued PEM certificate against tls.ca_file for client authentication, requires the ID as common name, refuses private key material and records the leaf fingerprint through the enrollment store. It is idempotent and replaces only that ID's binding. The deployment now binds every inventory executor from its deployed client.crt (public material only) as the service account: the dispatcher role and update-config.yml before the dispatcher runs with the requirement, and deploy-certs.yml on an enforcing dispatcher so a reissued certificate is re-bound. executor_enrollment_token renders [credentials] enrollment_token for executors bound by token instead, and the preflight refuses enforcement while an inventory executor has neither. Docs no longer imply a CA-issued certificate is sufficient. Fixes #415 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Problem
Turning on
dispatcher_require_client_certdisconnected every executor deployed withgenerate-certs.sh+deploy-certs.yml(executor ID is not enrolled, thencontrol session ownership mismatch). That caused a ~7 minute production outage, which was fixed by inserting binding rows by hand. A reissued client certificate would break the same way again.deploy/README.mdsuggested that giving executors certificates was enough.Cause
When client certificates are required,
cmd/dispatcher/main.goenablesEnforceEnrollment.enrollment.Store.Admit/Boundthen admits only executor IDs that have anexecutor_enrollmentsrow binding them to the SHA-256 of their leaf certificate. Nothing in the deployment created those rows. The executor role also had no way to pass an enrollment token.Fix
debuglet-dispatcher -bind-executor ID -bind-certificate PATH(newenrollment.CertificateFingerprintandStore.Bind, which reusesSetExecutorEnrollment):tls.ca_filewith clientAuth EKU, is not a CA, and has CN == executor ID (that is whatgenerate-certs.shand the enrollment signer issue)chainFingerprint-check-database/-upgrade-database/-init-databasetasks/bind-executors.yml, whendispatcher_require_client_certand the newdispatcher_bind_inventory_executors(defaulttrue) are on):client.crt(never a key) to{{ config_dir }}/dispatcher/executors/<id>.crt-bind-executoras the service user. It usesrunuserthe same wayupgrade-database.ymldoes, and runs the command directly when the play already runs as that user.site.yml): after rendering config and DB, before start/restart handlersupdate-config.yml: before restarting with the new configdeploy-certs.yml: on a dispatcher that already enforces, before the executors install their certs, so a reissued cert gets re-bound. It reports and skips when the dispatcher isn't installed yet or doesn't enforce yet.executor_enrollment_token: per-host secret, rendered as[credentials] enrollment_tokenonly when set (the template task is alreadyno_log). Executors that have a token are not bound from a certificate.--limit dispatcher. With binding disabled, every executor needs a token. Token format is checked underno_log.deploy/README.mddispatcher_require_client_certtextdocs/operations/remote-deployment.mdandexecutor-onboarding.md: enforcement binds IDs to fingerprints, the deployment binds inventory executors automatically, and reissuing a cert re-bindsRollout notes for an existing fleet
site.ymlfirst.deploy-certs.yml/update-config.ymlbind with the installed dispatcher binary, and older binaries don't have-bind-executor.make deploy-certs), setdispatcher_require_client_cert: true, then runupdate-config.yml. It binds before it restarts the dispatcher. Check the "Report the executor bindings" output.certs/executors/<id>/and runmake deploy-certs. The dispatcher re-binds before the executor installs and restarts with the new cert. The old cert is refused from the moment it is re-bound, so that executor is offline until its own restart later in the same run.-revoke-executor.Tests
go build,go vet,gofmtforcmd/dispatcherandinternal/dispatcher/enrollment.go test ./cmd/dispatcher ./internal/dispatcher/... ./internal/executor/configpass.internal/dispatcher/enrollment/bind_test.gocovers bind, idempotence, replace, other IDs untouched, tokens untouched, malformed input, and fingerprint verification (foreign CA, server-only EKU, CN mismatch, expired, CA leaf, private key in file, garbage, chain file).cmd/dispatcher/bind_executor_test.goruns the command end to end against a fresh database (bound, already bound, reissued replaces, refusals record nothing) and checks the flag pairing.-init-databaseexclusivity test now includes the new flags.deploy/test/ansible-render.shthroughdeploy/test/provisioner-check.sh(release built with./deploy/scripts/build-linux.shon a clean commit): all checks pass. New checks cover:deploy-certs.ymldefers binding before installdeploy-dispatcher.ymlbinds the inventory executor (fingerprint checked with openssl)update-config.ymlreports "already bound"deploy-certs.ymlNot exercised: a real systemd host, the
runuserpath as root, and a live executor reconnecting after a bind. The fixture runs as the service user without systemd.Fixes #415
🤖 Generated with Claude Code