Repository navigation
feat: Let organizations verify domains without a project - #1007
Open
mattdjenkinson wants to merge 5 commits into
Open
mattdjenkinson wants to merge 5 commits into
mattdjenkinson wants to merge 5 commits into
Conversation
An organization could prove it owned a domain only by adding it as a Domain in one of its projects. Email domain joining then depended on that project, and the company's email domain sat in a project it served nothing from. OrganizationDomain is a new resource in the organization's namespace, named after its domain. Milo publishes a random DNS TXT value for it, looks the record up (often at first, then backing off), and sets Verified once it finds it. A verified domain stays verified. - The webhook rejects public suffixes, public email providers, upper case, names that aren't the domain, and other organizations' namespaces. - Email domain joining accepts a domain an OrganizationDomain or a project Domain verifies, and reconciles the organization when an OrganizationDomain becomes verified or is removed. - Owners can add and remove domains through resourcemanager-admin; readers and organization viewers can see them. - --organization-domain-record-prefix sets the TXT record name's label, _milo-verification by default. - Everything is behind the EmailDomainJoining feature gate.
Adds a Chainsaw test for the webhook's rejections, the published TXT record, domainName immutability, and email domain joining with an organization-verified domain: rejected while unverified, accepted once verified, an offer for a user at the domain only, and the offer withdrawn when the domain is removed. The admin marks the domain verified through the status subresource, since CI can't publish a DNS record.
project-suspension-lifecycle and the projectsuspension admission test both created the cluster-scoped test-user-admin binding and deleted it on cleanup. When they overlapped, the first to finish removed the other's access, and project-suspension-lifecycle failed with test-user forbidden to create namespaces. Give each test its own binding.
mattdjenkinson
requested review from
0xmc,
JoseSzycho,
kevwilliams,
savme and
scotwells
November 11, 2026 10:21
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.
Organizations can now verify a domain on their own, as an
OrganizationDomain, without adding it to one of their projects. Email domain joining accepts a domain that either an OrganizationDomain or a project Domain verifies.Until now, the only way to prove an organization owned
acme.comwas to add it as a Domain in one of its projects. That made a company list its email domain in a project, as if it served something there, and tied joining to that project: deleting the project paused joining. Raised in datum-cloud/cloud-portal#1808.The resource
spec.domainNameis immutable.co.uk;gmail.com;Verified=Trueand clearsstatus.verification. A verified domain stays verified, as project Domains do, so the record can be removed afterwards.RecordNotFound,RecordMismatchandLookupFailedreasons say why._milo-verificationby default. The new--organization-domain-record-prefixflag lets a deployment brand it, for example_datum-verification.Email domain joining
emaildomain.Verifier.VerifiedDomainsnow returns the organization's verified OrganizationDomains as well as the verified Domains in its projects. Both the Organization webhook and the joining controller use it, so either kind of verification works.Permissions
resourcemanager-admininherits the neworganizationdomain-admin. Datum'sownerrole inheritsresourcemanager-admin.resourcemanager-readerandresourcemanager.miloapis.com-organization-viewerinheritorganizationdomain-reader.resourcemanager.miloapis.com-organizationdomain, with Organization as its parent.Rollout
EmailDomainJoininggate, which is on in staging. With the gate off, the webhook refuses new OrganizationDomains and the controller leaves them alone.Testing
Controller: publishing the record, verifying once it appears (next to other values at the same name), waiting for the next attempt, mismatch and lookup failure reasons, the configurable prefix, and doing nothing with the gate off.
Webhook: each rejection above, subdomains, private public suffixes such as
acme.github.io, and the organization context on create and update.Verifier: OrganizationDomains are included, other organizations' domains are not, and an unreadable project doesn't hide a verified OrganizationDomain.
Joining controller: the OrganizationDomain watch's mapping and predicate.
End to end:
test/resource-management/organization-domainis a new Chainsaw test, and it passes againsttask dev:setup. It covers:domainNameimmutability;CI can't publish a DNS record, so the test marks the domain verified through the status subresource. The test environment authorizes with Kubernetes RBAC, so it doesn't cover which IAM roles grant access.
task generateis committed andgo test ./...passes.Also fixed
project-suspension-lifecycleand theprojectsuspensionadmission test both created a cluster-scopedtest-user-adminClusterRoleBinding, and each deleted it during cleanup. When the two ran at the same time, the first to finish removed the other's access. Adding a test changed the timing enough to expose this race, so each test now has its own binding.