Skip to content

Commit

Permalink
Implement PKI authentication
Browse files Browse the repository at this point in the history
This adds support for enteprise PKI mode where a central Certificate
Authority is used to issue certificates for the LXD daemons as well as
the clients.

To enable this, replace or add the following files on the daemon side:
 - server.crt (CA generated server certificate)
 - server.key (key for the above certificate)
 - server.ca (Certificate Authority root certificate)

And on the client side:
 - client.crt (CA generated client certificate)
 - client.key (key for the above certificate)
 - client.ca (Certificate Authority root certificate)

In such mode, only clients using certificates issued by the PKI will be
able to connect. Normal password authentication is still used on first
handshake.

Closes #1985

Signed-off-by: Stéphane Graber <stgraber@ubuntu.com>
  • Loading branch information
stgraber committed Jun 2, 2016
1 parent c68b59e commit 84d917b
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 16 deletions.
19 changes: 16 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,17 @@ func NewClient(config *Config, remote string) (*Client, error) {
info.ClientPEMKey = string(keyBytes)
}

// Read the client key (if it exists)
clientCaPath := path.Join(config.ConfigDir, "client.ca")
if shared.PathExists(clientCaPath) {
caBytes, err := ioutil.ReadFile(clientCaPath)
if err != nil {
return nil, err
}

info.ClientPEMCa = string(caBytes)
}

// Read the server certificate (if it exists)
serverCertPath := config.ServerCertPath(remote)
if shared.PathExists(serverCertPath) {
Expand Down Expand Up @@ -222,6 +233,8 @@ type ConnectInfo struct {
ClientPEMCert string
// ClientPEMKey is the PEM encoded private bytes of the client's key associated with its certificate
ClientPEMKey string
// ClientPEMCa is the PEM encoded client certificate authority (if any)
ClientPEMCa string
// ServerPEMCert is the PEM encoded server certificate that we are
// connecting to. It can be the empty string if we do not know the
// server's certificate yet.
Expand Down Expand Up @@ -264,8 +277,8 @@ func connectViaUnix(c *Client, remote *RemoteConfig) error {
return nil
}

func connectViaHttp(c *Client, remote *RemoteConfig, clientCert, clientKey, serverCert string) error {
tlsconfig, err := shared.GetTLSConfigMem(clientCert, clientKey, serverCert)
func connectViaHttp(c *Client, remote *RemoteConfig, clientCert, clientKey, clientCA, serverCert string) error {
tlsconfig, err := shared.GetTLSConfigMem(clientCert, clientKey, clientCA, serverCert)
if err != nil {
return err
}
Expand Down Expand Up @@ -307,7 +320,7 @@ func NewClientFromInfo(info ConnectInfo) (*Client, error) {
if strings.HasPrefix(info.RemoteConfig.Addr, "unix:") {
err = connectViaUnix(c, &info.RemoteConfig)
} else {
err = connectViaHttp(c, &info.RemoteConfig, info.ClientPEMCert, info.ClientPEMKey, info.ServerPEMCert)
err = connectViaHttp(c, &info.RemoteConfig, info.ClientPEMCert, info.ClientPEMKey, info.ClientPEMCa, info.ServerPEMCert)
}
if err != nil {
return nil, err
Expand Down
13 changes: 11 additions & 2 deletions doc/lxd-ssl-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ A CRL may also accompany the CA certificate.

In that mode, any connection to a LXD daemon will be done using the
preseeded CA certificate. If the server certificate isn't signed by the
CA, or if it has been revoked, the connection will simply go through the
normal authentication mechanism.
CA, the connection will simply go through the normal authentication
mechanism.

If the server certificate is valid and signed by the CA, then the
connection continues without prompting the user for the certificate.
Expand All @@ -67,6 +67,15 @@ it matches, the client certificate is added to the server's trust store
and the client can now connect to the server without having to provide
any additional credentials.

Enabling PKI mode is done by adding a client.ca file in the
client's configuration directory (~/.config/lxc) and a server.ca file in
the server's configuration directory (/var/lib/lxd). Then a client
certificate must be issued by the CA for the client and a server
certificate for the server. Those must then replace the existing
pre-generated files.

After this is done, restarting the server will have it run in PKI mode.

# Password prompt
To establish a new trust relationship, a password must be set on the
server and send by the client when adding itself.
Expand Down
2 changes: 1 addition & 1 deletion lxc/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (c *remoteCmd) flags() {

func getRemoteCertificate(address string) (*x509.Certificate, error) {
// Setup a permissive TLS config
tlsConfig, err := shared.GetTLSConfig("", "", nil)
tlsConfig, err := shared.GetTLSConfig("", "", "", nil)
if err != nil {
return nil, err
}
Expand Down
8 changes: 8 additions & 0 deletions lxd/api_1.0.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,17 @@ var api10 = []Command{

func api10Get(d *Daemon, r *http.Request) Response {
body := shared.Jmap{
/* List of API extensions in the order they were added
* Is considered an API extension, any new configuration keys,
* any new argument to a REST endpoint or any new REST endpoint.
*
* Extra authentication methods should also be included.
*/
"api_extensions": []string{
"syscall_filtering",
"auth_pki",
},

"api_status": "stable",
"api_version": shared.APIVersion,
}
Expand Down
2 changes: 1 addition & 1 deletion lxd/containers_post.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func createFromMigration(d *Daemon, req *containerPostReq) Response {
}
}

config, err := shared.GetTLSConfig("", "", cert)
config, err := shared.GetTLSConfig("", "", "", cert)
if err != nil {
c.Delete()
return err
Expand Down
26 changes: 22 additions & 4 deletions lxd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (d *Daemon) httpGetSync(url string, certificate string) (*lxd.Response, err
}
}

tlsConfig, err := shared.GetTLSConfig("", "", cert)
tlsConfig, err := shared.GetTLSConfig("", "", "", cert)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -170,7 +170,7 @@ func (d *Daemon) httpGetFile(url string, certificate string) (*http.Response, er
}
}

tlsConfig, err := shared.GetTLSConfig("", "", cert)
tlsConfig, err := shared.GetTLSConfig("", "", "", cert)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -212,7 +212,6 @@ func readMyCert() (string, string, error) {
certf := shared.VarPath("server.crt")
keyf := shared.VarPath("server.key")
shared.Log.Info("Looking for existing certificates", log.Ctx{"cert": certf, "key": keyf})

err := shared.FindOrGenCert(certf, keyf)

return certf, keyf, err
Expand All @@ -223,14 +222,17 @@ func (d *Daemon) isTrustedClient(r *http.Request) bool {
// Unix socket
return true
}

if r.TLS == nil {
return false
}

for i := range r.TLS.PeerCertificates {
if d.CheckTrustState(*r.TLS.PeerCertificates[i]) {
return true
}
}

return false
}

Expand All @@ -247,6 +249,7 @@ func isJSONRequest(r *http.Request) bool {

func (d *Daemon) isRecursionRequest(r *http.Request) bool {
recursionStr := r.FormValue("recursion")

recursion, err := strconv.Atoi(recursionStr)
if err != nil {
return false
Expand Down Expand Up @@ -798,6 +801,21 @@ func (d *Daemon) Init() error {
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
PreferServerCipherSuites: true,
}

if shared.PathExists(shared.VarPath("server.ca")) {
ca, err := shared.ReadCert(shared.VarPath("server.ca"))
if err != nil {
return err
}

caPool := x509.NewCertPool()
caPool.AddCert(ca)
tlsConfig.RootCAs = caPool
tlsConfig.ClientCAs = caPool

shared.Log.Info("LXD is in CA mode, only CA-signed certificates will be allowed")
}

tlsConfig.BuildNameToCertificate()

d.tlsConfig = tlsConfig
Expand Down Expand Up @@ -1009,8 +1027,8 @@ func (d *Daemon) CheckTrustState(cert x509.Certificate) bool {
shared.Log.Debug("Found cert", log.Ctx{"k": k})
return true
}
shared.Log.Debug("Client cert != key", log.Ctx{"k": k})
}

return false
}

Expand Down
2 changes: 1 addition & 1 deletion lxd/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ func imgPostURLInfo(d *Daemon, req imagePostReq, op *operation) error {
}

// Resolve the image URL
tlsConfig, err := shared.GetTLSConfig("", "", nil)
tlsConfig, err := shared.GetTLSConfig("", "", "", nil)
if err != nil {
return err
}
Expand Down
29 changes: 26 additions & 3 deletions shared/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ func initTLSConfig() *tls.Config {
func finalizeTLSConfig(tlsConfig *tls.Config, tlsRemoteCert *x509.Certificate) {
// Trusted certificates
if tlsRemoteCert != nil {
caCertPool := x509.NewCertPool()
caCertPool := tlsConfig.RootCAs
if caCertPool == nil {
caCertPool = x509.NewCertPool()
}

// Make it a valid RootCA
tlsRemoteCert.IsCA = true
Expand All @@ -66,7 +69,7 @@ func finalizeTLSConfig(tlsConfig *tls.Config, tlsRemoteCert *x509.Certificate) {
tlsConfig.BuildNameToCertificate()
}

func GetTLSConfig(tlsClientCertFile string, tlsClientKeyFile string, tlsRemoteCert *x509.Certificate) (*tls.Config, error) {
func GetTLSConfig(tlsClientCertFile string, tlsClientKeyFile string, tlsClientCAFile string, tlsRemoteCert *x509.Certificate) (*tls.Config, error) {
tlsConfig := initTLSConfig()

// Client authentication
Expand All @@ -79,11 +82,23 @@ func GetTLSConfig(tlsClientCertFile string, tlsClientKeyFile string, tlsRemoteCe
tlsConfig.Certificates = []tls.Certificate{cert}
}

if tlsClientCAFile != "" {
caCertificates, err := ioutil.ReadFile(tlsClientCAFile)
if err != nil {
return nil, err
}

caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCertificates)

tlsConfig.RootCAs = caPool
}

finalizeTLSConfig(tlsConfig, tlsRemoteCert)
return tlsConfig, nil
}

func GetTLSConfigMem(tlsClientCert string, tlsClientKey string, tlsRemoteCertPEM string) (*tls.Config, error) {
func GetTLSConfigMem(tlsClientCert string, tlsClientKey string, tlsClientCA string, tlsRemoteCertPEM string) (*tls.Config, error) {
tlsConfig := initTLSConfig()

// Client authentication
Expand All @@ -106,6 +121,14 @@ func GetTLSConfigMem(tlsClientCert string, tlsClientKey string, tlsRemoteCertPEM
return nil, err
}
}

if tlsClientCA != "" {
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM([]byte(tlsClientCA))

tlsConfig.RootCAs = caPool
}

finalizeTLSConfig(tlsConfig, tlsRemoteCert)

return tlsConfig, nil
Expand Down
2 changes: 1 addition & 1 deletion shared/simplestreams.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ type SimpleStreamsIndexStream struct {

func SimpleStreamsClient(url string, proxy func(*http.Request) (*url.URL, error)) (*SimpleStreams, error) {
// Setup a http client
tlsConfig, err := GetTLSConfig("", "", nil)
tlsConfig, err := GetTLSConfig("", "", "", nil)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit 84d917b

Please sign in to comment.