Переглянути джерело

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 6 роки тому
батько
коміт
547b993e07
2 змінених файлів з 51 додано та 0 видалено
  1. 27 0
      daemon/info.go
  2. 24 0
      integration/system/info_test.go

+ 27 - 0
daemon/info.go

@@ -68,6 +68,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
 		Isolation:          daemon.defaultIsolation,
 		Isolation:          daemon.defaultIsolation,
 	}
 	}
 
 
+	daemon.fillAPIInfo(v)
 	// Retrieve platform specific info
 	// Retrieve platform specific info
 	daemon.fillPlatformInfo(v, sysInfo)
 	daemon.fillPlatformInfo(v, sysInfo)
 	daemon.fillDriverInfo(v)
 	daemon.fillDriverInfo(v)
@@ -171,6 +172,32 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf
 	v.SecurityOptions = securityOptions
 	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 {
 func hostName() string {
 	hostname := ""
 	hostname := ""
 	if hn, err := os.Hostname(); err != nil {
 	if hn, err := os.Hostname(); err != nil {

+ 24 - 0
integration/system/info_test.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"fmt"
 	"testing"
 	"testing"
 
 
+	"github.com/docker/docker/internal/test/daemon"
 	"github.com/docker/docker/internal/test/request"
 	"github.com/docker/docker/internal/test/request"
 	"gotest.tools/assert"
 	"gotest.tools/assert"
 	is "gotest.tools/assert/cmp"
 	is "gotest.tools/assert/cmp"
@@ -40,3 +41,26 @@ func TestInfoAPI(t *testing.T) {
 		assert.Check(t, is.Contains(out, linePrefix))
 		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))
+	}
+}