Browse code

Add warning if REST API is accessible through an insecure connection

The remote API allows full privilege escalation and is equivalent to
having root access on the host. Because of this, the API should never
be accessible through an insecure connection (TCP without TLS, or TCP
without TLS verification).

Although a warning is already logged on startup if the daemon uses an
insecure configuration, this warning is not very visible (unless someone
decides to read the logs).

This patch attempts to make insecure configuration more visible by sending
back warnings through the API (which will be printed when using `docker info`).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2018/08/21 21:06:06
Showing 2 changed files
... ...
@@ -68,6 +68,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
68 68
 		Isolation:          daemon.defaultIsolation,
69 69
 	}
70 70
 
71
+	daemon.fillAPIInfo(v)
71 72
 	// Retrieve platform specific info
72 73
 	daemon.fillPlatformInfo(v, sysInfo)
73 74
 	daemon.fillDriverInfo(v)
... ...
@@ -171,6 +172,32 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf
171 171
 	v.SecurityOptions = securityOptions
172 172
 }
173 173
 
174
+func (daemon *Daemon) fillAPIInfo(v *types.Info) {
175
+	const warn string = `
176
+         Access to the remote API is equivalent to root access on the host. Refer
177
+         to the 'Docker daemon attack surface' section in the documentation for
178
+         more information: https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface`
179
+
180
+	cfg := daemon.configStore
181
+	for _, host := range cfg.Hosts {
182
+		// cnf.Hosts is normalized during startup, so should always have a scheme/proto
183
+		h := strings.SplitN(host, "://", 2)
184
+		proto := h[0]
185
+		addr := h[1]
186
+		if proto != "tcp" {
187
+			continue
188
+		}
189
+		if !cfg.TLS {
190
+			v.Warnings = append(v.Warnings, fmt.Sprintf("WARNING: API is accessible on http://%s without encryption.%s", addr, warn))
191
+			continue
192
+		}
193
+		if !cfg.TLSVerify {
194
+			v.Warnings = append(v.Warnings, fmt.Sprintf("WARNING: API is accessible on https://%s without TLS client verification.%s", addr, warn))
195
+			continue
196
+		}
197
+	}
198
+}
199
+
174 200
 func hostName() string {
175 201
 	hostname := ""
176 202
 	if hn, err := os.Hostname(); err != nil {
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"fmt"
6 6
 	"testing"
7 7
 
8
+	"github.com/docker/docker/internal/test/daemon"
8 9
 	"github.com/docker/docker/internal/test/request"
9 10
 	"gotest.tools/assert"
10 11
 	is "gotest.tools/assert/cmp"
... ...
@@ -40,3 +41,26 @@ func TestInfoAPI(t *testing.T) {
40 40
 		assert.Check(t, is.Contains(out, linePrefix))
41 41
 	}
42 42
 }
43
+
44
+func TestInfoAPIWarnings(t *testing.T) {
45
+	d := daemon.New(t)
46
+
47
+	client, err := d.NewClient()
48
+	assert.NilError(t, err)
49
+
50
+	d.StartWithBusybox(t, "--iptables=false", "-H=0.0.0.0:23756", "-H=unix://"+d.Sock())
51
+	defer d.Stop(t)
52
+
53
+	info, err := client.Info(context.Background())
54
+	assert.NilError(t, err)
55
+
56
+	stringsToCheck := []string{
57
+		"Access to the remote API is equivalent to root access",
58
+		"http://0.0.0.0:23756",
59
+	}
60
+
61
+	out := fmt.Sprintf("%+v", info)
62
+	for _, linePrefix := range stringsToCheck {
63
+		assert.Check(t, is.Contains(out, linePrefix))
64
+	}
65
+}