[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