[lxc-devel] [lxd/master] candid: Improve domain validation and pubkey

stgraber on Github lxc-bot at linuxcontainers.org
Wed Oct 10 00:42:00 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 828 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20181010/5708b914/attachment.bin>
-------------- next part --------------
From 1d19cda6730ca62c040dce3aca3f698901c10d96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 9 Oct 2018 20:18:24 -0400
Subject: [PATCH] candid: Improve domain validation and pubkey
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This introduces a new "candid.api.key" option which can be set to the
expected public key for the candid server.

Setting this key is effectively required if using a non-HTTPs server.
When using HTTPs, this is optional as LXD can fetch the key directly
from the candid server.

An issue with validation of domains is also fixed, effectively
preventing a candid token which doesn't include a domain for being
considered valid when a list of valid domains is set.

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 doc/api-extensions.md   |  5 +++
 doc/server.md           |  1 +
 lxd/api_1.0.go          |  5 ++-
 lxd/cluster/config.go   |  6 ++++
 lxd/daemon.go           | 70 ++++++++++++++++++++++++++++++++---------
 scripts/bash/lxd-client |  3 +-
 6 files changed, 73 insertions(+), 17 deletions(-)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index 93e4a2ec38..66f779f922 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -616,3 +616,8 @@ any further idmap tracking and remapping on the volume.
 
 This can be used to share data between isolated containers after
 attaching it to the container which requires write access.
+
+## candid\_config\_key
+This introduces a new `candid.api.key` option which allows for setting
+the expected public key for the endpoint, allowing for safe use of a
+HTTP-only candid server.
diff --git a/doc/server.md b/doc/server.md
index 240c11ca19..6eb22ef692 100644
--- a/doc/server.md
+++ b/doc/server.md
@@ -11,6 +11,7 @@ currently supported:
 Key                             | Type      | Default   | API extension            | Description
 :--                             | :---      | :------   | :------------            | :----------
 backups.compression\_algorithm  | string    | gzip      | backup\_compression      | Compression algorithm to use for new images (bzip2, gzip, lzma, xz or none)
+candid.api.key                  | string    | -         | candid\_config\_key      | Public key of the candid server (required for HTTP-only servers)
 candid.api.url                  | string    | -         | candid\_authentication   | URL of the the external authentication endpoint using Candid
 candid.expiry                   | integer   | 3600      | candid\_config           | Candid macaroon expiry in seconds
 candid.domains                  | string    | -         | candid\_config           | Comma-separated list of allowed Candid domains (empty string means all domains are valid)
diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index 8c4e8453b3..bfc5caf030 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -388,6 +388,8 @@ func doApi10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str
 			fallthrough
 		case "candid.expiry":
 			fallthrough
+		case "candid.api.key":
+			fallthrough
 		case "candid.api.url":
 			candidChanged = true
 		case "images.auto_update_interval":
@@ -427,10 +429,11 @@ func doApi10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str
 
 	if candidChanged {
 		endpoint := clusterConfig.CandidEndpoint()
+		endpointKey := clusterConfig.CandidEndpointKey()
 		expiry := clusterConfig.CandidExpiry()
 		domains := clusterConfig.CandidDomains()
 
-		err := d.setupExternalAuthentication(endpoint, expiry, domains)
+		err := d.setupExternalAuthentication(endpoint, endpointKey, expiry, domains)
 		if err != nil {
 			return err
 		}
diff --git a/lxd/cluster/config.go b/lxd/cluster/config.go
index bc2596d822..a7930d71de 100644
--- a/lxd/cluster/config.go
+++ b/lxd/cluster/config.go
@@ -70,6 +70,11 @@ func (c *Config) CandidEndpoint() string {
 	return c.m.GetString("candid.api.url")
 }
 
+// CandidEndpointKey returns the public key for the API endpoint
+func (c *Config) CandidEndpointKey() string {
+	return c.m.GetString("candid.api.key")
+}
+
 // CandidExpiry returns the cookie expiry of the macaroon.
 func (c *Config) CandidExpiry() int64 {
 	return c.m.GetInt64("candid.expiry")
@@ -221,6 +226,7 @@ var ConfigSchema = config.Schema{
 	"core.proxy_https":               {},
 	"core.proxy_ignore_hosts":        {},
 	"core.trust_password":            {Hidden: true, Setter: passwordSetter},
+	"candid.api.key":                 {},
 	"candid.api.url":                 {},
 	"candid.domains":                 {},
 	"candid.expiry":                  {Type: config.Int64, Default: "3600"},
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 32554f0ba7..53fd95f672 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -90,20 +90,27 @@ type IdentityClientWrapper struct {
 	ValidDomains []string
 }
 
-func newIdentityClientWrapper(client identchecker.IdentityClient) *IdentityClientWrapper {
-	return &IdentityClientWrapper{client: client, ValidDomains: []string{}}
-}
-
 func (m *IdentityClientWrapper) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) {
 	return m.client.IdentityFromContext(ctx)
 }
 
 func (m *IdentityClientWrapper) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) {
-	// Validate domains since Candid doesn't do that
+	// Extract the domain from the username
 	fields := strings.SplitN(declared["username"], "@", 2)
-	// If no ValidDomains have been specified, all are valid
-	if len(m.ValidDomains) > 0 && len(fields) > 1 && !shared.StringInSlice(fields[1], m.ValidDomains) {
-		return nil, fmt.Errorf("Unsupported domain: %s", fields[1])
+
+	// Only validate domain if we have a list of valid domains
+	if len(m.ValidDomains) > 0 {
+		// If no domain was provided by candid, reject the request
+		if len(fields) < 2 {
+			logger.Warnf("Failed candid client authentication: no domain provided")
+			return nil, fmt.Errorf("Missing domain in candid reply")
+		}
+
+		// Check that it was a valid domain
+		if !shared.StringInSlice(fields[1], m.ValidDomains) {
+			logger.Warnf("Failed candid client authentication: untrusted domain \"%s\"", fields[1])
+			return nil, fmt.Errorf("Untrusted candid domain")
+		}
 	}
 
 	return m.client.DeclaredIdentity(ctx, declared)
@@ -620,6 +627,7 @@ func (d *Daemon) init() error {
 	var candidExpiry int64
 	candidDomains := ""
 	candidEndpoint := ""
+	candidEndpointKey := ""
 	maasAPIURL := ""
 	maasAPIKey := ""
 	maasMachine := ""
@@ -648,6 +656,7 @@ func (d *Daemon) init() error {
 			config.ProxyHTTPS(), config.ProxyHTTP(), config.ProxyIgnoreHosts(),
 		)
 		candidEndpoint = config.CandidEndpoint()
+		candidEndpointKey = config.CandidEndpointKey()
 		candidExpiry = config.CandidExpiry()
 		candidDomains = config.CandidDomains()
 		maasAPIURL, maasAPIKey = config.MAASController()
@@ -657,7 +666,7 @@ func (d *Daemon) init() error {
 		return err
 	}
 
-	err = d.setupExternalAuthentication(candidEndpoint, candidExpiry, candidDomains)
+	err = d.setupExternalAuthentication(candidEndpoint, candidEndpointKey, candidExpiry, candidDomains)
 	if err != nil {
 		return err
 	}
@@ -861,17 +870,24 @@ func (d *Daemon) Stop() error {
 }
 
 // Setup external authentication
-func (d *Daemon) setupExternalAuthentication(authEndpoint string, expiry int64, domains string) error {
+func (d *Daemon) setupExternalAuthentication(authEndpoint string, authPubkey string, expiry int64, domains string) error {
+	// Parse the list of domains
 	authDomains := []string{}
 	for _, domain := range strings.Split(domains, ",") {
+		if domain == "" {
+			continue
+		}
+
 		authDomains = append(authDomains, strings.TrimSpace(domain))
 	}
 
+	// Allow disable external authentication
 	if authEndpoint == "" {
 		d.externalAuth = nil
 		return nil
 	}
 
+	// Setup the candid client
 	idmClient, err := candidclient.New(candidclient.NewParams{
 		BaseURL: authEndpoint,
 	})
@@ -879,19 +895,40 @@ func (d *Daemon) setupExternalAuthentication(authEndpoint string, expiry int64,
 		return err
 	}
 
-	idmClientWrapper := newIdentityClientWrapper(idmClient)
-	idmClientWrapper.ValidDomains = authDomains
+	idmClientWrapper := &IdentityClientWrapper{
+		client:       idmClient,
+		ValidDomains: authDomains,
+	}
 
+	// Generate an internal private key
 	key, err := bakery.GenerateKey()
 	if err != nil {
 		return err
 	}
 
-	pkLocator := httpbakery.NewThirdPartyLocator(nil, nil)
-	if strings.HasPrefix(authEndpoint, "http://") {
-		pkLocator.AllowInsecure()
+	pkCache := bakery.NewThirdPartyStore()
+	pkLocator := httpbakery.NewThirdPartyLocator(nil, pkCache)
+	if authPubkey != "" {
+		// Parse the public key
+		pkKey := bakery.Key{}
+		err := pkKey.UnmarshalText([]byte(authPubkey))
+		if err != nil {
+			return err
+		}
+
+		// Add the key information
+		pkCache.AddInfo(authEndpoint, bakery.ThirdPartyInfo{
+			PublicKey: bakery.PublicKey{Key: pkKey},
+			Version:   3,
+		})
+
+		// Allow http URLs if we have a public key set
+		if strings.HasPrefix(authEndpoint, "http://") {
+			pkLocator.AllowInsecure()
+		}
 	}
 
+	// Setup the bakery
 	bakery := identchecker.NewBakery(identchecker.BakeryParams{
 		Key:            key,
 		Location:       authEndpoint,
@@ -904,11 +941,14 @@ func (d *Daemon) setupExternalAuthentication(authEndpoint string, expiry int64,
 			},
 		},
 	})
+
+	// Store our settings
 	d.externalAuth = &externalAuth{
 		endpoint: authEndpoint,
 		expiry:   expiry,
 		bakery:   bakery,
 	}
+
 	return nil
 }
 
diff --git a/scripts/bash/lxd-client b/scripts/bash/lxd-client
index 95caea3a2c..1d8b95da88 100644
--- a/scripts/bash/lxd-client
+++ b/scripts/bash/lxd-client
@@ -67,7 +67,8 @@ _have lxc && {
     global_keys="backups.compression_algorithm,
       core.https_address core.https_allowed_credentials \
       core.https_allowed_headers core.https_allowed_methods \
-      core.https_allowed_origin candid.api.url candid.expiry candid.domains \
+      core.https_allowed_origin candid.api.url candid.api.key candid.expiry \
+      candid.domains \
       core.proxy_https core.proxy_http core.proxy_ignore_hosts \
       core.trust_password core.debug_address cluster.offline_threshold \
       images.auto_update_cached images.auto_update_interval \


More information about the lxc-devel mailing list