Skip to content

Public cluster links - #18197

Open
kadinsayani wants to merge 15 commits into
canonical:mainfrom
kadinsayani:cluster-links-unidirectional-unauthenticated
Open

Public cluster links#18197
kadinsayani wants to merge 15 commits into
canonical:mainfrom
kadinsayani:cluster-links-unidirectional-unauthenticated

Conversation

@kadinsayani

@kadinsayani kadinsayani commented Apr 27, 2026

Copy link
Copy Markdown
Member

Summary

Adds public cluster link support. A public cluster link lets one cluster (A) reach another cluster (B) without presenting a client certificate, relying solely on TLS certificate pinning to authenticate B. B is completely unaware of the link; no identity or link row is created on B's side. This is useful when B exposes resources publicly, or when anonymous-style read access to B is sufficient.

Depends on #18195.

API extension: cluster_links_public

How it works

Creating a public link is a two-phase POST /1.0/cluster/links on A:

  1. Fetch. A POST with a name, "type": "public" and remote_address (no cluster_certificate) creates a pending link. A fetches B's TLS certificate, verifies that the address actually serves the LXD API, and returns the certificate's fingerprint and PEM encoding (new ClusterLinkCertificate response type) for the user to verify. The certificate is held in volatile.pending_certificate but is not yet pinned, so the link stays inert.
  2. Confirm. A second POST with the same name and remote_address, this time including the cluster_certificate from step 1, pins the certificate and activates the link. The resubmitted certificate must match the one fetched in step 1; a mismatch is rejected and the link remains pending.

The CLI performs both phases, prompting for confirmation of the fingerprint in between.

Example workflow

# On Cluster A: create a public link to B
lxc cluster link create image-host --public --remote-address 10.0.0.1:8443
# Confirm the certificate fingerprint when prompted

# Verify on A
lxc cluster link list
lxc cluster link show image-host    # type: public

# Cleanup (A only — B is unaware)
lxc cluster link delete image-host

@github-actions github-actions Bot added Documentation Documentation needs updating API Changes to the REST API labels Apr 27, 2026
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 5 times, most recently from 8691581 to 364ff9d Compare April 28, 2026 16:04
@simondeziel

Copy link
Copy Markdown
Member

How it works

1. Cluster A specifies B's address. LXD fetches B's TLS certificate via a new GET /1.0/cluster/links/certificate endpoint and returns the fingerprint for user confirmation.

2. A stores the link locally with B's pinned certificate. B is never contacted beyond the initial cert fetch.

This TOFU (Trust On First Use) with user verification provides authentication AFAIK. Is the "unauthenticated" bit here meaning there is no mTLS going on between the 2 clusters?

@kadinsayani

Copy link
Copy Markdown
Member Author

How it works

1. Cluster A specifies B's address. LXD fetches B's TLS certificate via a new GET /1.0/cluster/links/certificate endpoint and returns the fingerprint for user confirmation.

2. A stores the link locally with B's pinned certificate. B is never contacted beyond the initial cert fetch.

This TOFU (Trust On First Use) with user verification provides authentication AFAIK. Is the "unauthenticated" bit here meaning there is no mTLS going on between the 2 clusters?

Yes, exactly. "unauthenticated" means no mTLS; cluster A does not present a client certificate when connecting to cluster B.

@simondeziel

simondeziel commented Apr 28, 2026

Copy link
Copy Markdown
Member

How it works

1. Cluster A specifies B's address. LXD fetches B's TLS certificate via a new GET /1.0/cluster/links/certificate endpoint and returns the fingerprint for user confirmation.

2. A stores the link locally with B's pinned certificate. B is never contacted beyond the initial cert fetch.

This TOFU (Trust On First Use) with user verification provides authentication AFAIK. Is the "unauthenticated" bit here meaning there is no mTLS going on between the 2 clusters?

Yes, exactly. "unauthenticated" means no mTLS; cluster A does not present a client certificate when connecting to cluster B.

OK good, that's better in terms of security and aligns nicely with the other widely known TOFU model popularized by SSH.

That said, I feels like a misnomer to call it "unauthenticated" then. "Unidirectional cluster link"? "One way cluster link"?

@kadinsayani

Copy link
Copy Markdown
Member Author

How it works

1. Cluster A specifies B's address. LXD fetches B's TLS certificate via a new GET /1.0/cluster/links/certificate endpoint and returns the fingerprint for user confirmation.

2. A stores the link locally with B's pinned certificate. B is never contacted beyond the initial cert fetch.

This TOFU (Trust On First Use) with user verification provides authentication AFAIK. Is the "unauthenticated" bit here meaning there is no mTLS going on between the 2 clusters?

Yes, exactly. "unauthenticated" means no mTLS; cluster A does not present a client certificate when connecting to cluster B.

OK good, that's better in terms of security and aligns nicely with the other widely known TOFU model popularized by SSH.

That said, I feels like a misnomer to call it "unauthenticated" then. "Unidirectional cluster link"? "One way cluster link"?

Since we already have unidirectional as another type, how about "public"? @tomponline previously suggested this.

@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 2 times, most recently from 925eeb0 to 66c2686 Compare April 28, 2026 20:01
Comment thread test/suites/clustering.sh Fixed
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 3 times, most recently from 6ad2c54 to 01beba1 Compare April 28, 2026 20:22
@simondeziel

Copy link
Copy Markdown
Member

Yes, exactly. "unauthenticated" means no mTLS; cluster A does not present a client certificate when connecting to cluster B.

OK good, that's better in terms of security and aligns nicely with the other widely known TOFU model popularized by SSH.
That said, I feels like a misnomer to call it "unauthenticated" then. "Unidirectional cluster link"? "One way cluster link"?

Since we already have unidirectional as another type, how about "public"? @tomponline previously suggested this.

"Public cluster link" sounds good. There is also "anonymous cluster link" where the connecting side doesn't present its TLS cert.

@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 2 times, most recently from 0bdb8d2 to dec1f01 Compare April 28, 2026 20:31

@simondeziel simondeziel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The test LGTM, thanks!

Comment thread lxd/db/cluster/update.go Outdated
Comment thread lxc/cluster_link.go Outdated
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 3 times, most recently from 698caad to e9cee31 Compare April 28, 2026 21:12

@simondeziel simondeziel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Other than the "unauthenticated" bit that I find misleading, this LGTM (with a tiny nit).

Comment thread lxc/cluster_link.go Outdated
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 3 times, most recently from 810f2bf to 72e828e Compare April 29, 2026 20:11
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch from 72e828e to 711e68d Compare May 7, 2026 14:35
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 2 times, most recently from dd2bab9 to e7ab8cc Compare July 27, 2026 19:52
@kadinsayani
kadinsayani marked this pull request as ready for review July 28, 2026 15:14
@kadinsayani
kadinsayani marked this pull request as draft July 28, 2026 15:16
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 2 times, most recently from 5d166cb to ee39952 Compare July 28, 2026 18:33
Comment thread lxd/api_cluster_link.go Fixed
@kadinsayani
kadinsayani force-pushed the cluster-links-unidirectional-unauthenticated branch 2 times, most recently from 9bf95f8 to a897514 Compare July 28, 2026 18:46
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new public cluster link type that allows one cluster to reach another without presenting a client certificate, relying on pinned server TLS certificates and a two-phase (fetch/confirm) creation flow. This extends the existing cluster-links feature set (and builds on unidirectional links) while explicitly preventing use of public links for replication features that require authenticated writes.

Changes:

  • Introduces API extension cluster_links_public, new link type public, and a new ClusterLinkCertificate response for the pending fetch phase.
  • Implements server-side pending/confirm handling with volatile pending state (volatile.pending_certificate, volatile.pending_address) and public-link connection args (no client cert).
  • Updates CLI, docs, and integration tests to support and validate the new public flow (including rejection paths and replication/config-time guards).

Reviewed changes

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

Show a summary per file
File Description
test/suites/clustering.sh Adds an end-to-end integration test covering public cluster link creation/confirmation, canonicalization, rejection paths, and replication restrictions.
test/includes/test-groups.sh Adds the new clustering test to the cluster test group.
shared/version/api.go Registers the new API extension cluster_links_public.
shared/api/cluster.go Adds ClusterLinkTypePublic, RemoteAddress to request payload, and ClusterLinkCertificate response type.
lxd/metadata/configuration.json Documents new volatile config keys for pending public links.
lxd/db/cluster/cluster_links.go Adds DB type code mapping for public cluster links.
lxd/cluster/cluster_link.go Adds server verification of remote being a LXD API, and public-link connection args (no client cert) for link refresh/connect flows.
lxd/api_replicators.go Rejects public cluster links for replicators at config validation time.
lxd/api_project.go Rejects public cluster links for project replication config (replica.cluster).
lxd/api_cluster_link.go Adds pending/confirm public link request modes, validation, handlers, and volatile key validation.
lxc/cluster_link.go Adds --public + --remote-address CLI flow with certificate fingerprint confirmation and cleanup behavior.
doc/rest-api.yaml Documents the new ClusterLinkCertificate type and remote_address field in the REST API schema.
doc/metadata.txt Documents new volatile config keys in generated metadata docs.
doc/howto/cluster_links_manage.md Documents deletion behavior for public links.
doc/howto/cluster_links_create.md Adds “Create a public cluster link” how-to section and updates link-type overview.
doc/explanation/clusters.md Updates cluster-links explanation to include public links and their connection process.
doc/api-extensions.md Documents the cluster_links_public extension and its two-phase behavior and restrictions.
client/lxd_cluster.go Adds client support for pending public link creation and extension gating for public link creation.
client/interfaces.go Extends InstanceServer interface with CreateClusterLinkPendingPublic.

Comment thread test/suites/clustering.sh Outdated
Comment thread lxd/cluster/cluster_link.go
Add GetPublicClusterLinkConnectionArgs and update
RefreshClusterLinkVolatileAddresses to branch on link type so
public links present no client certificate.

Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
… its certificate

Add VerifyClusterLinkServer, which connects with the pinned certificate
(no client certificate presented) and queries /1.0, and
wire it into CheckClusterLinkCertificate so a matching-but-non-LXD
endpoint is rejected.

Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Public cluster links follow a two-phase creation lifecycle, matching
bidirectional/unidirectional links: a pending-create phase and a
confirm phase.

A POST with a name and remote_address (no cluster_certificate) fetches
the remote cluster's certificate, verifies it's actually serving the
LXD API, and creates a pending link. The fetched certificate is held
in volatile.pending_certificate config, not yet pinned, and
volatile.addresses is not set, so the link stays inert until
confirmed. It returns the fingerprint and PEM certificate for user
verification.

A second POST with the same name/remote_address plus the returned
cluster_certificate pins the certificate for the first time and
activates the link by setting volatile.addresses, after verifying the
resubmitted certificate's fingerprint matches the one fetched during
pending creation; a mismatch is rejected. The certificate is not
re-fetched from the remote at this stage. Confirming a link that
doesn't exist, isn't public, or has already been confirmed is also
rejected.

Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Public cluster links present no client certificate, so the remote cluster
cannot authenticate the connection. Replication requires authenticated
writes, so reject public links at config validation time rather than
failing mid-operation with an opaque authentication error.

Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
CreateClusterLink checks the cluster_links_public extension when the
request is a public link, in addition to the base cluster_links check.

CreateClusterLinkPendingPublic mirrors CreateIdentityClusterLinkToken's
pattern for bidirectional/unidirectional pending links: it creates a
pending public cluster link and returns the fetched certificate
fingerprint/PEM for the caller to display for user verification before
confirming via CreateClusterLink.

Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Signed-off-by: Kadin Sayani <kadin.sayani@canonical.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants