[management] Expire unvalidated custom domain registrations (#7497)

Prevent unvalidated registrations from reserving domain names indefinitely.

Give pending registrations a 48-hour validation window and clean up expired entries at startup and every 60 minutes. Emit CustomDomainValidationExpired for each deletion and preserve registrations referenced by services.

Reject validation after expiry and prevent concurrent validation from recreating deleted registrations. Normalize domain names with the shared parser before registration.

Migrate existing pending registrations to receive a fresh 48-hour validation window.
This commit is contained in:
Maycon Santos
2026-09-11 17:57:58 +02:00
committed by GitHub
parent ec0c36b0e7
commit b789ffbb9f
23 changed files with 941 additions and 60 deletions
+17 -4
View File
@@ -5712,6 +5712,10 @@ func (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, dom
Type: domain.TypeCustom,
Validated: validated,
}
if !validated {
expiresAt := time.Now().UTC().Add(domain.ValidationTTL)
newDomain.ValidationExpiresAt = &expiresAt
}
result := s.db.Create(newDomain)
if result.Error != nil {
// The unique index is the last guard when two requests clear the
@@ -5733,12 +5737,21 @@ func (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, dom
return newDomain, nil
}
// UpdateCustomDomain completes validation only while the original registration is pending.
func (s *SqlStore) UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error) {
d.AccountID = accountID
result := s.db.Select("*").Save(d)
if !d.Validated {
return nil, status.Errorf(status.InvalidArgument, "custom domain update must complete validation")
}
result := s.db.WithContext(ctx).Model(&domain.Domain{}).
Where(accountAndIDQueryCondition, accountID, d.ID).
Where("domain = ? AND target_cluster = ?", d.Domain, d.TargetCluster).
Where("validated = ? AND validation_expires_at > ?", false, time.Now().UTC()).
Update("validated", true)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to update reverse proxy custom domain to store: %v", result.Error)
return nil, status.Errorf(status.Internal, "failed to update reverse proxy custom domain to store")
return nil, fmt.Errorf("validate custom domain in store: %w", result.Error)
}
if result.RowsAffected == 0 {
return nil, status.Errorf(status.PreconditionFailed, "custom domain registration is no longer pending validation")
}
return d, nil