[LXD] Token based remote connection

Project LXD
Status Implemented
Author(s) @monstermunchkin
Approver(s) @stgraber
Release 4.23
Internal ID LX012

Abstract

This allows adding new remotes using a token.

Rationale

Adding a new remote currently involves either setting up a trust password, or trusting the client before it adds the remote.

From a security perspective, the trust password is not ideal as it can be brute-forced, and the password itself often is weak.

If the remote needs to trust the client first, the remote needs to know the client’s certificate and trust it using lxc config trust add <cert>. After this, the client needs to add the remote using lxc remote add <name> <url>.

With a token based approach, the remote generates a token which the client uses to add the remote. There are no passwords, certificates or URLs needed.

Specification

Design

User level

A remote can generate a new token using lxc config trust add (without specifying a certificate). It’s interactive and will prompt for a name, and then return a token. This token is a base64 encoded representation of a CertificateAddToken containing the client name, server addresses, and the join secret.

A client can then use this token with lxc remote add <remote> <token> to add the new remote.

Once the remote has been added, the token expires. It is also possible for the remote to revoke a token using lxc config trust revoke-token <name>. A list of active tokens can shown using lxc config trust list-tokens.

API level

When running lxc config trust add, the POST /1.0/certificates endpoint is called, with Token set to true. In this case, the Type needs to be client, and Certificate needs to be empty.

LXD will then create a new token operation and put the request in its metadata, similar to what is done for the cluster token.

When consuming the token, the CLI will use a normal POST /1.0/certificates and pass the token as the Password field. LXD will detect it’s a token, will locate the operation and if one is found, will add the certificate to the trust store using the request information stored in the operation’s metadata.

API changes

type CertificateAddToken struct {
	// The name of the new client
	// Example: lxd02
	ClientName string `json:"client_name" yaml:"client_name"`

	// The fingerprint of the network certificate
	// Example: 57bb0ff4340b5bb28517e062023101adf788c37846dc8b619eb2c3cb4ef29436
	Fingerprint string `json:"fingerprint" yaml:"fingerprint"`

	// The addresses of the server
	// Example: ["10.98.30.229:8443"]
	Addresses []string `json:"addresses" yaml:"addresses"`

	// The random join secret.
	// Example: 2b2284d44db32675923fe0d2020477e0e9be11801ff70c435e032b97028c35cd
	Secret string `json:"secret" yaml:"secret"`
}
type CertificatesPost struct {
	CertificatePut `yaml:",inline"`

	// Server trust password (used to add an untrusted client)
	// Example: blah
	Password string `json:"password" yaml:"password"`

	// Whether to use token
	// Example: true
	Token bool `json:"token" yaml:"token"`
}

A new function for creating and returning a join token needs to be added:

 func CreateCertificateToken(certificate api.CertificatesPost) (op Operation, err error)

CLI changes

New commands:

  • lxc config trust list-tokens for listing all active tokens
  • lxc config trust revoke-token <name> for revoking an active token

lxc config trust add will prompt for a name if no certificate is provided, and then return a token.

lxc remote add will support tokens.

Database changes

No database changes.

Upgrade handling

No upgrade handling.

Further information

To make the CLI consistent, lxc cluster add will gain an interactive mode where it asks for the cluster name. The prompt can be skipped by using --name <name> which allows for scripting.

1 Like

At the API level, we need to deal with POST /1.0/certificates
I think the easiest there will be to add a new token bool field to CertificatesPost.

When that is set to true, we’ll fail if certificate is set or if type is something other than client.

Then we’ll create a new token operation and put the request in its metadata, similar to what we do for the cluster token.

The token should be using a format similar to that of ClusterMemberJoinToken, at least containing:

  • Fingerprint
  • Addresses
  • Secret

When consuming the token, the CLI will use a normal POST /1.0/certificates and pass the token as the Password field. LXD will detect it’s a token, will locate the operation and if one is found, will add the certificate to the trust store using the request information stored in the operation’s metadata.

Should mention that lxc remote add will be interactive whereas lxc cluster add --name NAME allows for scripting.

Otherwise spec looks good, approved.

Commented on PR but will comment here for visibility.

Can tokens have a prefix like LT: (LXD Token)?

This way if a user copies and pastes a join token into a trust password input (say in a web browser) we can easily identify them and perform logic as applicable. Otherwise we will need the mutually exclusive fields Trust password or Token (The odds of hitting a “trust password” that has the same prefix seems ridiculously small).

Github tokens also implement a prefix, though i’m not sure the reasoning behind it. Probably the phasing out of a “git password”.

Responded on Github

@stgraber I’m playing around with this API and I was able to generate the secret. What do I do with this secret to make it look like the join token spit out by the CLI?

https://github.com/lxc/lxd/blob/2e4d9822d7bd779eb3a1eed9539810f6b142f74a/shared/api/operation.go#L73 is the logic in the CLI to pack it as a full token.

It extracts secret, fingerprint and addresses from the operation data, then grabs the client name from the request data and grabs the expiry if one is set.

That then gets put into a struct named CertificateAddToken which is then converted to JSON.
The JSON field names can be found in the struct definition here: https://github.com/lxc/lxd/blob/2e4d9822d7bd779eb3a1eed9539810f6b142f74a/shared/api/certificate.go#L98

Hope that helps!

Hey @stgraber

Thank you! Yes As you were replying I was reading the source code and saw that it was just Base64 encoding, of the following json

{
  "client_name": "name",
  "fingerprint": "fingerprint",
  "addresses": ["address"],
  "secret": "generatedsecret"
}

Thank you for the prompt reply!