From 547b993e07330f3e74cba935975fce05e8661381 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 21 Aug 2018 14:06:06 +0200 Subject: [PATCH] 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 --- daemon/info.go | 27 +++++++++++++++++++++++++++ integration/system/info_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/daemon/info.go b/daemon/info.go index cc9ad8ac61..9dcfb95f03 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -68,6 +68,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) { Isolation: daemon.defaultIsolation, } + daemon.fillAPIInfo(v) // Retrieve platform specific info daemon.fillPlatformInfo(v, sysInfo) daemon.fillDriverInfo(v) @@ -171,6 +172,32 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf v.SecurityOptions = securityOptions } +func (daemon *Daemon) fillAPIInfo(v *types.Info) { + const warn string = ` + Access to the remote API is equivalent to root access on the host. Refer + to the 'Docker daemon attack surface' section in the documentation for + more information: https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface` + + cfg := daemon.configStore + for _, host := range cfg.Hosts { + // cnf.Hosts is normalized during startup, so should always have a scheme/proto + h := strings.SplitN(host, "://", 2) + proto := h[0] + addr := h[1] + if proto != "tcp" { + continue + } + if !cfg.TLS { + v.Warnings = append(v.Warnings, fmt.Sprintf("WARNING: API is accessible on http://%s without encryption.%s", addr, warn)) + continue + } + if !cfg.TLSVerify { + v.Warnings = append(v.Warnings, fmt.Sprintf("WARNING: API is accessible on https://%s without TLS client verification.%s", addr, warn)) + continue + } + } +} + func hostName() string { hostname := "" if hn, err := os.Hostname(); err != nil { diff --git a/integration/system/info_test.go b/integration/system/info_test.go index 2a05dfbb74..b8bdcf0049 100644 --- a/integration/system/info_test.go +++ b/integration/system/info_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/docker/docker/internal/test/daemon" "github.com/docker/docker/internal/test/request" "gotest.tools/assert" is "gotest.tools/assert/cmp" @@ -40,3 +41,26 @@ func TestInfoAPI(t *testing.T) { assert.Check(t, is.Contains(out, linePrefix)) } } + +func TestInfoAPIWarnings(t *testing.T) { + d := daemon.New(t) + + client, err := d.NewClient() + assert.NilError(t, err) + + d.StartWithBusybox(t, "--iptables=false", "-H=0.0.0.0:23756", "-H=unix://"+d.Sock()) + defer d.Stop(t) + + info, err := client.Info(context.Background()) + assert.NilError(t, err) + + stringsToCheck := []string{ + "Access to the remote API is equivalent to root access", + "http://0.0.0.0:23756", + } + + out := fmt.Sprintf("%+v", info) + for _, linePrefix := range stringsToCheck { + assert.Check(t, is.Contains(out, linePrefix)) + } +}