Skip to content

Commit c000cb6

Browse files
Add authenticated TLS support for API
Docker-DCO-1.1-Signed-off-by: Johannes 'fish' Ziemke <[email protected]> (github: discordianfish)
1 parent 83ffc28 commit c000cb6

19 files changed

Lines changed: 812 additions & 50 deletions

File tree

api/client.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"bufio"
55
"bytes"
6+
"crypto/tls"
67
"encoding/base64"
78
"encoding/json"
89
"errors"
@@ -57,8 +58,8 @@ func (cli *DockerCli) getMethod(name string) (func(...string) error, bool) {
5758
return method.Interface().(func(...string) error), true
5859
}
5960

60-
func ParseCommands(proto, addr string, args ...string) error {
61-
cli := NewDockerCli(os.Stdin, os.Stdout, os.Stderr, proto, addr)
61+
func ParseCommands(proto, addr string, tlsConfig *tls.Config, args ...string) error {
62+
cli := NewDockerCli(os.Stdin, os.Stdout, os.Stderr, proto, addr, tlsConfig)
6263

6364
if len(args) > 0 {
6465
method, exists := cli.getMethod(args[0])
@@ -2026,6 +2027,13 @@ func (cli *DockerCli) CmdLoad(args ...string) error {
20262027
return nil
20272028
}
20282029

2030+
func (cli *DockerCli) dial() (net.Conn, error) {
2031+
if cli.tlsConfig != nil && cli.proto != "unix" {
2032+
return tls.Dial(cli.proto, cli.addr, cli.tlsConfig)
2033+
}
2034+
return net.Dial(cli.proto, cli.addr)
2035+
}
2036+
20292037
func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) {
20302038
params := bytes.NewBuffer(nil)
20312039
if data != nil {
@@ -2078,7 +2086,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b
20782086
} else if method == "POST" {
20792087
req.Header.Set("Content-Type", "plain/text")
20802088
}
2081-
dial, err := net.Dial(cli.proto, cli.addr)
2089+
dial, err := cli.dial()
20822090
if err != nil {
20832091
if strings.Contains(err.Error(), "connection refused") {
20842092
return nil, -1, ErrConnectionRefused
@@ -2140,7 +2148,7 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h
21402148
}
21412149
}
21422150

2143-
dial, err := net.Dial(cli.proto, cli.addr)
2151+
dial, err := cli.dial()
21442152
if err != nil {
21452153
if strings.Contains(err.Error(), "connection refused") {
21462154
return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?")
@@ -2196,7 +2204,7 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea
21962204
req.Header.Set("Content-Type", "plain/text")
21972205
req.Host = cli.addr
21982206

2199-
dial, err := net.Dial(cli.proto, cli.addr)
2207+
dial, err := cli.dial()
22002208
if err != nil {
22012209
if strings.Contains(err.Error(), "connection refused") {
22022210
return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?")
@@ -2388,7 +2396,7 @@ func readBody(stream io.ReadCloser, statusCode int, err error) ([]byte, int, err
23882396
return body, statusCode, nil
23892397
}
23902398

2391-
func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *DockerCli {
2399+
func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string, tlsConfig *tls.Config) *DockerCli {
23922400
var (
23932401
isTerminal = false
23942402
terminalFd uintptr
@@ -2412,6 +2420,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *Doc
24122420
err: err,
24132421
isTerminal: isTerminal,
24142422
terminalFd: terminalFd,
2423+
tlsConfig: tlsConfig,
24152424
}
24162425
}
24172426

@@ -2424,4 +2433,5 @@ type DockerCli struct {
24242433
err io.Writer
24252434
isTerminal bool
24262435
terminalFd uintptr
2436+
tlsConfig *tls.Config
24272437
}

api/server.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"bufio"
55
"bytes"
66
"code.google.com/p/go.net/websocket"
7+
"crypto/tls"
8+
"crypto/x509"
79
"encoding/base64"
810
"encoding/json"
911
"expvar"
@@ -1129,9 +1131,8 @@ func changeGroup(addr string, nameOrGid string) error {
11291131

11301132
// ListenAndServe sets up the required http.Server and gets it listening for
11311133
// each addr passed in and does protocol specific checking.
1132-
func ListenAndServe(proto, addr string, eng *engine.Engine, logging, enableCors bool, dockerVersion string, socketGroup string) error {
1133-
r, err := createRouter(eng, logging, enableCors, dockerVersion)
1134-
1134+
func ListenAndServe(proto, addr string, job *engine.Job) error {
1135+
r, err := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version"))
11351136
if err != nil {
11361137
return err
11371138
}
@@ -1151,17 +1152,43 @@ func ListenAndServe(proto, addr string, eng *engine.Engine, logging, enableCors
11511152
return err
11521153
}
11531154

1155+
if proto != "unix" && (job.GetenvBool("Tls") || job.GetenvBool("TlsVerify")) {
1156+
tlsCert := job.Getenv("TlsCert")
1157+
tlsKey := job.Getenv("TlsKey")
1158+
cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey)
1159+
if err != nil {
1160+
return fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?",
1161+
tlsCert, tlsKey, err)
1162+
}
1163+
tlsConfig := &tls.Config{
1164+
NextProtos: []string{"http/1.1"},
1165+
Certificates: []tls.Certificate{cert},
1166+
}
1167+
if job.GetenvBool("TlsVerify") {
1168+
certPool := x509.NewCertPool()
1169+
file, err := ioutil.ReadFile(job.Getenv("TlsCa"))
1170+
if err != nil {
1171+
return fmt.Errorf("Couldn't read CA certificate: %s", err)
1172+
}
1173+
certPool.AppendCertsFromPEM(file)
1174+
1175+
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
1176+
tlsConfig.ClientCAs = certPool
1177+
}
1178+
l = tls.NewListener(l, tlsConfig)
1179+
}
1180+
11541181
// Basic error and sanity checking
11551182
switch proto {
11561183
case "tcp":
1157-
if !strings.HasPrefix(addr, "127.0.0.1") {
1184+
if !strings.HasPrefix(addr, "127.0.0.1") && !job.GetenvBool("TlsVerify") {
11581185
log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
11591186
}
11601187
case "unix":
11611188
if err := os.Chmod(addr, 0660); err != nil {
11621189
return err
11631190
}
1164-
1191+
socketGroup := job.Getenv("SocketGroup")
11651192
if socketGroup != "" {
11661193
if err := changeGroup(addr, socketGroup); err != nil {
11671194
if socketGroup == "docker" {
@@ -1197,7 +1224,7 @@ func ServeApi(job *engine.Job) engine.Status {
11971224
protoAddrParts := strings.SplitN(protoAddr, "://", 2)
11981225
go func() {
11991226
log.Printf("Listening for HTTP on %s (%s)\n", protoAddrParts[0], protoAddrParts[1])
1200-
chErrors <- ListenAndServe(protoAddrParts[0], protoAddrParts[1], job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version"), job.Getenv("SocketGroup"))
1227+
chErrors <- ListenAndServe(protoAddrParts[0], protoAddrParts[1], job)
12011228
}()
12021229
}
12031230

contrib/host-integration/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func main() {
7070
bufErr := bytes.NewBuffer(nil)
7171

7272
// Instanciate the Docker CLI
73-
cli := docker.NewDockerCli(nil, bufOut, bufErr, "unix", "/var/run/docker.sock")
73+
cli := docker.NewDockerCli(nil, bufOut, bufErr, "unix", "/var/run/docker.sock", false, nil)
7474
// Retrieve the container info
7575
if err := cli.CmdInspect(flag.Arg(0)); err != nil {
7676
// As of docker v0.6.3, CmdInspect always returns nil

docker/docker.go

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package main
22

33
import (
4+
"crypto/tls"
5+
"crypto/x509"
46
"fmt"
7+
"io/ioutil"
58
"log"
69
"os"
710
"strings"
@@ -16,6 +19,16 @@ import (
1619
"github.com/dotcloud/docker/utils"
1720
)
1821

22+
const (
23+
defaultCaFile = "ca.pem"
24+
defaultKeyFile = "key.pem"
25+
defaultCertFile = "cert.pem"
26+
)
27+
28+
var (
29+
dockerConfDir = os.Getenv("HOME") + "/.docker/"
30+
)
31+
1932
func main() {
2033
if selfPath := utils.SelfPath(); strings.Contains(selfPath, ".dockerinit") {
2134
// Running in init mode
@@ -43,6 +56,11 @@ func main() {
4356
flExecDriver = flag.String([]string{"e", "-exec-driver"}, "native", "Force the docker runtime to use a specific exec driver")
4457
flHosts = opts.NewListOpts(api.ValidateHost)
4558
flMtu = flag.Int([]string{"#mtu", "-mtu"}, 0, "Set the containers network MTU; if no value is provided: default to the default route MTU or 1500 if no default route is available")
59+
flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by tls-verify flags")
60+
flTlsVerify = flag.Bool([]string{"-tlsverify"}, false, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)")
61+
flCa = flag.String([]string{"-tlscacert"}, dockerConfDir+defaultCaFile, "Trust only remotes providing a certificate signed by the CA given here")
62+
flCert = flag.String([]string{"-tlscert"}, dockerConfDir+defaultCertFile, "Path to TLS certificate file")
63+
flKey = flag.String([]string{"-tlskey"}, dockerConfDir+defaultKeyFile, "Path to TLS key file")
4664
)
4765
flag.Var(&flDns, []string{"#dns", "-dns"}, "Force docker to use specific DNS servers")
4866
flag.Var(&flHosts, []string{"H", "-host"}, "tcp://host:port, unix://path/to/socket, fd://* or fd://socketfd to use in daemon mode. Multiple sockets can be specified")
@@ -73,6 +91,7 @@ func main() {
7391
if *flDebug {
7492
os.Setenv("DEBUG", "1")
7593
}
94+
7695
if *flDaemon {
7796
if flag.NArg() != 0 {
7897
flag.Usage()
@@ -140,6 +159,12 @@ func main() {
140159
job.SetenvBool("EnableCors", *flEnableCors)
141160
job.Setenv("Version", dockerversion.VERSION)
142161
job.Setenv("SocketGroup", *flSocketGroup)
162+
163+
job.SetenvBool("Tls", *flTls)
164+
job.SetenvBool("TlsVerify", *flTlsVerify)
165+
job.Setenv("TlsCa", *flCa)
166+
job.Setenv("TlsCert", *flCert)
167+
job.Setenv("TlsKey", *flKey)
143168
if err := job.Run(); err != nil {
144169
log.Fatal(err)
145170
}
@@ -148,14 +173,53 @@ func main() {
148173
log.Fatal("Please specify only one -H")
149174
}
150175
protoAddrParts := strings.SplitN(flHosts.GetAll()[0], "://", 2)
151-
if err := api.ParseCommands(protoAddrParts[0], protoAddrParts[1], flag.Args()...); err != nil {
152-
if sterr, ok := err.(*utils.StatusError); ok {
176+
177+
var (
178+
errc error
179+
tlsConfig tls.Config
180+
)
181+
tlsConfig.InsecureSkipVerify = true
182+
183+
// If we should verify the server, we need to load a trusted ca
184+
if *flTlsVerify {
185+
*flTls = true
186+
certPool := x509.NewCertPool()
187+
file, err := ioutil.ReadFile(*flCa)
188+
if err != nil {
189+
log.Fatalf("Couldn't read ca cert %s: %s", *flCa, err)
190+
}
191+
certPool.AppendCertsFromPEM(file)
192+
tlsConfig.RootCAs = certPool
193+
tlsConfig.InsecureSkipVerify = false
194+
}
195+
196+
// If tls is enabled, try to load and send client certificates
197+
if *flTls || *flTlsVerify {
198+
_, errCert := os.Stat(*flCert)
199+
_, errKey := os.Stat(*flKey)
200+
if errCert == nil && errKey == nil {
201+
*flTls = true
202+
cert, err := tls.LoadX509KeyPair(*flCert, *flKey)
203+
if err != nil {
204+
log.Fatalf("Couldn't load X509 key pair: %s. Key encrypted?", err)
205+
}
206+
tlsConfig.Certificates = []tls.Certificate{cert}
207+
}
208+
}
209+
210+
if *flTls || *flTlsVerify {
211+
errc = api.ParseCommands(protoAddrParts[0], protoAddrParts[1], &tlsConfig, flag.Args()...)
212+
} else {
213+
errc = api.ParseCommands(protoAddrParts[0], protoAddrParts[1], nil, flag.Args()...)
214+
}
215+
if errc != nil {
216+
if sterr, ok := errc.(*utils.StatusError); ok {
153217
if sterr.Status != "" {
154218
log.Println(sterr.Status)
155219
}
156220
os.Exit(sterr.StatusCode)
157221
}
158-
log.Fatal(err)
222+
log.Fatal(errc)
159223
}
160224
}
161225
}

0 commit comments

Comments
 (0)