Skip to content

fix(domain): point _ans-badge DNS record URL at transparency log#23

Merged
kperry-godaddy merged 1 commit into
godaddy:mainfrom
fobispo-tc:fix/ans-badge-tl-public-url
May 28, 2026
Merged

fix(domain): point _ans-badge DNS record URL at transparency log#23
kperry-godaddy merged 1 commit into
godaddy:mainfrom
fobispo-tc:fix/ans-badge-tl-public-url

Conversation

@fobispo-tc
Copy link
Copy Markdown
Contributor

Summary

The _ans-badge TXT record's url= field was incorrectly set to the agent's own endpoint URL instead of the Transparency Log badge endpoint.

Before (broken):

_ans-badge.agent.example.com TXT "v=ans-badge1; version=1.0.0; url=https://agent.example.com/mcp"

After (correct):

_ans-badge.agent.example.com TXT "v=ans-badge1; version=1.0.0; url=https://tl.example.org/v1/agents/<agentId>"

Problem

Badge-verifying clients (the SDK's verify package) parse the _ans-badge TXT record and fetch the badge from the url= field. The SDK documents this field as "the URL to fetch the badge from the transparency log" (ans-sdk-go/verify/badge_record.go:34), but the RA was populating it with the agent's own endpoint URL from reg.Endpoints[0].AgentURL. This meant verifiers would try to fetch a badge from the agent itself — which doesn't serve badges — instead of from the TL.

Fix

  • Add tl-client.public-base-url config field to the RA. This is the externally-reachable TL URL (e.g. https://tl.example.org), which may differ from the internal tl-client.base-url (e.g. http://ans-tl:18081 in Docker Compose).
  • ComputeRequiredDNSRecords now accepts the TL public URL and constructs the badge URL as <public-base-url>/v1/agents/<agentId>.
  • Falls back to the agent endpoint URL when public-base-url is unset, preserving backwards compatibility for deployments that haven't configured it yet.

Changes

File Change
internal/config/config.go Add PublicBaseURL to TLClient struct; default to BaseURL in Validate()
internal/domain/dnsrecords.go Accept tlPublicBaseURL param; use it for badge URL when set
internal/domain/dnsrecords_test.go Update existing tests + 2 new tests for TL URL and fallback
internal/ra/service/registration.go Add tlPublicBaseURL field + WithTLPublicBaseURL() builder + getter
internal/ra/service/lifecycle.go Pass s.tlPublicBaseURL at 3 call sites
internal/ra/service/v1event.go Pass s.tlPublicBaseURL at 1 call site
internal/ra/handler/dto.go Thread TL URL through mapAgentDetails and buildRegistrationPendingBlock
internal/ra/handler/lifecycle.go Pass TL URL from service getter
internal/ra/handler/v1registration.go Thread TL URL through V1 handler chain
cmd/ans-ra/main.go Wire cfg.TLClient.PublicBaseURL via .WithTLPublicBaseURL()
config/ra-local.yaml Document new config option (commented out)
config/ra-docker.yaml Document new config option (commented out)

Configuration

tl-client:
  base-url: "http://ans-tl:18081"          # internal — used for RA→TL HTTP calls
  public-base-url: "https://tl.example.org" # external — used in _ans-badge DNS records

When public-base-url is omitted, it defaults to base-url.

Test plan

  • All existing tests pass (go test ./... — 23 packages, 0 failures)
  • New test: TestComputeRequiredDNSRecords_BadgeURLPointsToTL — verifies badge URL uses TL endpoint
  • New test: TestComputeRequiredDNSRecords_BadgeFallbackWithoutTLURL — verifies fallback to agent URL
  • Deploy with public-base-url set and verify new registrations produce correct _ans-badge records

@fobispo-tc fobispo-tc marked this pull request as ready for review May 22, 2026 00:32
Copilot AI review requested due to automatic review settings May 22, 2026 00:32
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for publishing _ans-badge DNS TXT records that point to an externally reachable Transparency Log (TL) URL (useful when TL is behind a reverse proxy/CDN).

Changes:

  • Extends domain.ComputeRequiredDNSRecords to accept a tlPublicBaseURL and uses it to build badge URLs.
  • Wires TL public URL from config → service → handlers and revoke/verify flows.
  • Adds config + docs for tl-client.public-base-url and tests for TL badge URL behavior.

Reviewed changes

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

Show a summary per file
File Description
internal/ra/service/v1event.go Passes TL public URL into DNS record computation for revoke events
internal/ra/service/registration.go Stores TL public URL on service; adds setter/getter
internal/ra/service/lifecycle.go Uses TL public URL when computing expected DNS records for verify/revoke
internal/ra/handler/v1registration.go Threads TL public URL into v1 pending-block DNS record computation
internal/ra/handler/lifecycle.go Threads TL public URL into v2 detail DTO mapping
internal/ra/handler/dto.go Threads TL public URL into v2 pending-block DNS record computation
internal/domain/dnsrecords_test.go Updates existing tests for new signature; adds TL badge URL tests
internal/domain/dnsrecords.go Implements TL-based badge URL generation
internal/config/config.go Adds public-base-url config and defaults it to base-url
config/ra-local.yaml Documents tl-client.public-base-url option
config/ra-docker.yaml Documents tl-client.public-base-url option
cmd/ans-ra/main.go Wires config TL public URL into service initialization

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

Comment on lines +74 to +77
badgeURL := reg.Endpoints[0].AgentURL
if tlPublicBaseURL != "" && reg.AgentID != "" {
badgeURL = strings.TrimRight(tlPublicBaseURL, "/") + "/v1/agents/" + reg.AgentID
}
Comment thread internal/ra/service/registration.go Outdated
Comment on lines +212 to +213
func (s *RegistrationService) WithTLPublicBaseURL(url string) *RegistrationService {
s.tlPublicBaseURL = url
Copy link
Copy Markdown

Copilot AI left a comment

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 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread internal/config/config.go Outdated
Comment on lines +348 to +350
if c.TLClient.PublicBaseURL == "" {
c.TLClient.PublicBaseURL = c.TLClient.BaseURL
}
@csnitker-godaddy
Copy link
Copy Markdown
Collaborator

Thanks for the contribution! The changes look good to me. One blocker before merge: this repo requires GPG-signed commits on main, and the two commits on this branch aren't verified yet. Could you re-sign and force-push? The Signed-off-by: trailers you already have are fine — it's just the GPG signature that's missing.

Copy link
Copy Markdown

@kperry-godaddy kperry-godaddy left a comment

Choose a reason for hiding this comment

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

Agree with @csnitker-godaddy - thanks for contributing.
Three things worth addressing before merge — inline comments below.

Comment thread internal/config/config.go
BaseURL string `koanf:"base-url"`
// PublicBaseURL is the TL's externally-reachable URL used in
// _ans-badge DNS TXT records. When empty, defaults to BaseURL.
PublicBaseURL string `koanf:"public-base-url"`
Copy link
Copy Markdown

@kperry-godaddy kperry-godaddy May 26, 2026

Choose a reason for hiding this comment

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

Doc claims defaulting that doesn't exist in code.

The comment above this field says "When empty, defaults to BaseURL," but RAConfig.Validate() (lines 309-352) has no such defaulting. Operators who pull this PR without setting public-base-url in YAML keep emitting _ans-badge records pointing at the agent's own URL — the exact bug this PR is fixing.

Two clean options:

  • Required + validated (recommended): in Validate(), return an error when PublicBaseURL is empty, and reject anything that isn't https:// with no userinfo/query/fragment.
  • Default to BaseURL: assign c.TLClient.PublicBaseURL = c.TLClient.BaseURL when empty, so behavior matches the doc.

Either way, the field doc and the validator need to agree. The PR description's "Falls back to the agent endpoint URL when public-base-url is unset, preserving backwards compatibility" line should also be revised — the prior behavior was a bug, not a stable contract.

Comment thread internal/domain/dnsrecords.go Outdated
if len(reg.Endpoints) > 0 {
badgeURL := reg.Endpoints[0].AgentURL
if tlPublicBaseURL != "" && reg.AgentID != "" {
if joined, err := url.JoinPath(tlPublicBaseURL, "v1", "agents", reg.AgentID); err == nil {
Copy link
Copy Markdown

@kperry-godaddy kperry-godaddy May 26, 2026

Choose a reason for hiding this comment

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

Silent url.JoinPath error swallow.

The err == nil guard silently falls back to reg.Endpoints[0].AgentURL (the buggy URL this PR is fixing). A typo in public-base-url — missing scheme, embedded query string, stray whitespace — is undetectable until external badge verification fails in production.

Pair this with config-load validation: parse PublicBaseURL once in Validate() and reject anything that isn't a clean https://host[/path]. Once Validate() enforces parseability, this JoinPath becomes a guaranteed-success invariant — either drop the error guard, or convert it to a panic so an internal regression is caught loudly rather than papered over by emitting the wrong URL into the immutable TL.

Comment thread cmd/ans-ra/main.go
}).WithDNSVerifier(dnsVerifier).
WithServerCertificateAuthority(serverCA)
WithServerCertificateAuthority(serverCA).
WithTLPublicBaseURL(cfg.TLClient.PublicBaseURL)
Copy link
Copy Markdown

@kperry-godaddy kperry-godaddy May 26, 2026

Choose a reason for hiding this comment

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

No startup log of effective PublicBaseURL.

The TL outbox-worker startup log at lines 276-279 emits tlBaseURL (internal) but not PublicBaseURL. At 3am when oncall is debugging "badges point to the wrong place," they cannot tell pre-PR-23 from post-PR-23-but-misconfigured without SSH-ing the box and reading YAML.

Suggested addition near config load:

if cfg.TLClient.PublicBaseURL == "" {
    logger.Warn().Msg("tl-client.public-base-url is unset; _ans-badge DNS records will fall back to agent URL")
} else {
    logger.Info().
        Str("tlPublicBaseURL", cfg.TLClient.PublicBaseURL).
        Str("tlBaseURL", cfg.TLClient.BaseURL).
        Msg("transparency log endpoints configured")
}

Collapses MTTR for badge-verification incidents from 15-30 minutes (config inspection) to a 30-second log scan.

@fobispo-tc fobispo-tc force-pushed the fix/ans-badge-tl-public-url branch from c826fc8 to 4aafe81 Compare May 27, 2026 15:59
kperry-godaddy
kperry-godaddy previously approved these changes May 27, 2026
@kperry-godaddy kperry-godaddy dismissed their stale review May 27, 2026 17:08

Looks like the linter is failing

@kperry-godaddy
Copy link
Copy Markdown

@fobispo-tc - looks good here. It looks like the linter is failing. Once we get a clean build. We will get this merged. Thanks for fixing things up so far.

    The _ans-badge TXT record's url= field was set to the agent's own
    endpoint URL instead of the Transparency Log badge endpoint.

    Add tl-client.public-base-url config (required, validated as https).
    When set, badge records point to <public-base-url>/v1/agents/<agentId>.
    Falls back to the agent endpoint when unset.

    Signed-off-by: Francisco Obispo <fobispo@tucows.com>

Signed-off-by: Francisco Obispo <fobispo@tucows.com>
@fobispo-tc fobispo-tc force-pushed the fix/ans-badge-tl-public-url branch from 4aafe81 to 4f70e18 Compare May 27, 2026 23:55
@kperry-godaddy
Copy link
Copy Markdown

Thanks for the contribution! 🎉

@kperry-godaddy kperry-godaddy merged commit 31adf40 into godaddy:main May 28, 2026
2 checks passed
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.

4 participants