Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lxc: Prevent accept-certificate flag when using trust token #14149

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
109 changes: 87 additions & 22 deletions lxc/remote.go
Copy link
Member

Choose a reason for hiding this comment

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

Please can you add more detail to the commit message explaining what you fixed.

Copy link
Member

Choose a reason for hiding this comment

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

If a remote is added, and then removed from the local config (lxc remote delete)
and the certificate remains trusted on the remote server, when remote is added
again it will fail because it is already trusted.

Is this the current state now and what you've fixed, or what you are changing it to be?

I struggled to understand the change here.

Copy link
Member

Choose a reason for hiding this comment

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

Because:

when remote is added again it will fail because it is already trusted.

Sounds like the wrong behaviour to me, so wanted to understand if this is new or what is being fixed?

Copy link
Member Author

@MusicDin MusicDin Sep 30, 2024

Choose a reason for hiding this comment

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

This is the current behavior. If remote is removed, and readded, it will fail when trying to add client certificate into remote's trust store, because it is already present there - failing with Client is already trusted error, and remote not being added.

IMO, we should instead just continue and add the remote.

Example of current behaviour (LXD running in a VM v2 is our remote server):

$ lxc ls
+------+---------+------------------------+-------------------------------------------------+-----------------+-----------+
| NAME |  STATE  |          IPV4          |                      IPV6                       |      TYPE       | SNAPSHOTS |
+------+---------+------------------------+-------------------------------------------------+-----------------+-----------+
| v2   | RUNNING | 10.70.164.239 (enp5s0) | fd42:2bf4:ef35:8266:216:3eff:fea8:9da4 (enp5s0) | VIRTUAL-MACHINE | 0         |
+------+---------+------------------------+-------------------------------------------------+-----------------+-----------+

# Add the remote (initially).
$ token=$(lxc exec v2 -- lxc config trust add --name test --quiet)
$ lxc remote add test $token

# Remove the remote.
$ lxc remote rm test

# Add the remote (again). The client certificate is already present in remote's trust store.
$ token=$(lxc exec v2 -- lxc config trust add --name test --quiet)
$ lxc remote add test $token
Error: Failed to create certificate: Client is already trusted

Copy link
Member

Choose a reason for hiding this comment

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

Does lxc remote add test work after doing lxc remote rm test?

Copy link
Member Author

@MusicDin MusicDin Sep 30, 2024

Choose a reason for hiding this comment

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

Yes, if target URL is specified:

lxc remote add 10.70.164.239
# or
lxc remote add test 10.70.164.239

When token is used, the address is extracted from the token itself (if specific URL is not provided), so it should work even when token is provided.

Copy link
Member

Choose a reason for hiding this comment

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

Got you. Will the new token still get expired once its used (even though its not really used)?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the token is not consumed.

Copy link
Member

Choose a reason for hiding this comment

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

Should it not be? From the user's PoV we've used it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I think that makes sense. Will fix that.

Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (c *cmdRemoteAdd) findProject(d lxd.InstanceServer, project string) (string
return project, nil
}

func (c *cmdRemoteAdd) runToken(server string, token string, rawToken *api.CertificateAddToken) error {
func (c *cmdRemoteAdd) runToken(addr string, server string, token string, rawToken *api.CertificateAddToken) error {
conf := c.global.conf

if !conf.HasClientCertificate() {
Expand All @@ -164,6 +164,12 @@ func (c *cmdRemoteAdd) runToken(server string, token string, rawToken *api.Certi
}
}

// If address is provided, use token on that specific address.
if addr != "" {
return c.addRemoteFromToken(addr, server, token, rawToken.Fingerprint)
}

// Otherwise, iterate over all addresses within the token.
for _, addr := range rawToken.Addresses {
addr = fmt.Sprintf("https://%s", addr)

Expand All @@ -179,6 +185,7 @@ func (c *cmdRemoteAdd) runToken(server string, token string, rawToken *api.Certi
return nil
}

// Finally, fallback to manual input.
fmt.Println(i18n.G("All server addresses are unavailable"))
fmt.Print(i18n.G("Please provide an alternate server address (empty to abort):") + " ")

Expand Down Expand Up @@ -248,16 +255,34 @@ func (c *cmdRemoteAdd) addRemoteFromToken(addr string, server string, token stri
return api.StatusErrorf(http.StatusServiceUnavailable, "%s: %w", i18n.G("Unavailable remote server"), err)
}

req := api.CertificatesPost{}
if d.HasExtension("explicit_trust_token") {
req.TrustToken = token
} else {
req.Password = token
srv, _, err := d.GetServer()
if err != nil {
return err
}

err = d.CreateCertificate(req)
if err != nil {
return fmt.Errorf(i18n.G("Failed to create certificate: %w"), err)
if srv.Auth != "trusted" {
req := api.CertificatesPost{}
if d.HasExtension("explicit_trust_token") {
req.TrustToken = token
} else {
req.Password = token
}

// Add client certificate to trust store.
err = d.CreateCertificate(req)
if err != nil {
return fmt.Errorf(i18n.G("Failed to create certificate: %w"), err)
}

// And check if trusted now.
srv, _, err = d.GetServer()
if err != nil {
return err
}

if srv.Auth != "trusted" {
return errors.New(i18n.G("Server doesn't trust us after authentication"))
}
}

// Handle project.
Expand Down Expand Up @@ -294,6 +319,21 @@ func (c *cmdRemoteAdd) run(cmd *cobra.Command, args []string) error {
return errors.New(i18n.G("Remote address must not be empty"))
}

// Trust token cannot be used when auth type is set to OIDC.
if c.flagToken != "" && c.flagAuthType == "oidc" {
return errors.New(i18n.G("Trust token cannot be used with OIDC authentication"))
}

// Trust token cannot be used for public remotes.
if c.flagToken != "" && c.flagPublic {
return errors.New(i18n.G("Trust token cannot be used for public remotes"))
}

// Certificate cannot be blindly accepted when using a trust token.
Copy link
Member

Choose a reason for hiding this comment

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

Please can you update the comment to explain the why of this statement.

if c.flagToken != "" && c.flagAcceptCert {
return errors.New(i18n.G("The --accept-certificate flag is not supported when adding a remote using a trust token"))
}

// Validate the server name.
if strings.Contains(server, ":") {
return errors.New(i18n.G("Remote names may not contain colons"))
Expand All @@ -319,9 +359,16 @@ func (c *cmdRemoteAdd) run(cmd *cobra.Command, args []string) error {
conf.Remotes = map[string]config.Remote{}
}

// Check if the first argument is a trust token. In such case, we need to
// decode it and use it to connect to the remote.
rawToken, err := shared.CertificateTokenDecode(addr)
if err == nil {
return c.runToken(server, addr, rawToken)
// Certificate cannot be blindly accepted when using a trust token.
if c.flagAcceptCert {
Copy link
Member

Choose a reason for hiding this comment

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

can we re-structure this commit to avoid needing to check the same logic twice and duplicating the error message?

return errors.New(i18n.G("The --accept-certificate flag is not supported when adding a remote using a trust token"))
}

return c.runToken("", server, addr, rawToken)
}

// Complex remote URL parsing
Expand Down Expand Up @@ -437,6 +484,16 @@ func (c *cmdRemoteAdd) run(cmd *cobra.Command, args []string) error {
return conf.SaveConfig(c.global.confPath)
}

// Handle adding a remote with trust token.
if c.flagToken != "" {
rawToken, err := shared.CertificateTokenDecode(c.flagToken)
if err != nil {
return fmt.Errorf(i18n.G("Failed to decode trust token: %w"), err)
}

return c.runToken(addr, server, c.flagToken, rawToken)
}

// Check if the system CA worked for the TLS connection
var certificate *x509.Certificate
if err != nil {
Expand All @@ -449,22 +506,32 @@ func (c *cmdRemoteAdd) run(cmd *cobra.Command, args []string) error {

// Handle certificate prompt
if certificate != nil {
// Prompt for certificate acceptance if user did not allow us to blindly
// accept the remote certificate.
if !c.flagAcceptCert {
digest := shared.CertFingerprint(certificate)

fmt.Printf("%s: %s\n", i18n.G("Certificate fingerprint"), digest)
fmt.Print(i18n.G("ok (y/n/[fingerprint])?") + " ")
line, err := shared.ReadStdin()
if err != nil {
return err
}
for {
line, err := shared.ReadStdin()
if err != nil {
return err
}

if string(line) != digest {
// Continue with adding the remote if digest matches, or the user
// confirmed a fingerprint.
if string(line) == digest || strings.ToLower(string(line[0])) == i18n.G("y") {
break
}

// Error out if the user didn't confirm the fingerprint.
if len(line) < 1 || strings.ToLower(string(line[0])) == i18n.G("n") {
return errors.New(i18n.G("Server certificate NACKed by user"))
} else if strings.ToLower(string(line[0])) != i18n.G("y") {
return errors.New(i18n.G("Please type 'y', 'n' or the fingerprint:"))
}

// Ask again for any other invalid input.
fmt.Print(i18n.G("Please type 'y', 'n' or the fingerprint:"))
}
}

Expand Down Expand Up @@ -567,11 +634,9 @@ func (c *cmdRemoteAdd) run(cmd *cobra.Command, args []string) error {
// use the token instead and prompt for it if not present.
if d.(lxd.InstanceServer).HasExtension("explicit_trust_token") && c.flagPassword == "" {
// Prompt for trust token.
if c.flagToken == "" {
c.flagToken, err = c.global.asker.AskString(fmt.Sprintf(i18n.G("Trust token for %s: "), server), "", nil)
if err != nil {
return err
}
c.flagToken, err = c.global.asker.AskString(fmt.Sprintf(i18n.G("Trust token for %s: "), server), "", nil)
if err != nil {
return err
}

req.TrustToken = c.flagToken
Expand Down
Loading
Loading