Skip to content

Commit ec764f7

Browse files
authored
Merge pull request #3754 from filipochnik/docker-caps
Add an option to add and drop capabilities in the Docker driver
2 parents 9d006ec + 35d9331 commit ec764f7

File tree

16 files changed

+1912
-1
lines changed

16 files changed

+1912
-1
lines changed

client/driver/docker.go

+54
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ const (
9999
dockerImageRemoveDelayConfigOption = "docker.cleanup.image.delay"
100100
dockerImageRemoveDelayConfigDefault = 3 * time.Minute
101101

102+
// dockerCapsWhitelistConfigOption is the key for setting the list of
103+
// allowed Linux capabilities
104+
dockerCapsWhitelistConfigOption = "docker.caps.whitelist"
105+
dockerCapsWhitelistConfigDefault = dockerBasicCaps
106+
102107
// dockerTimeout is the length of time a request can be outstanding before
103108
// it is timed out.
104109
dockerTimeout = 5 * time.Minute
@@ -109,6 +114,12 @@ const (
109114
// dockerAuthHelperPrefix is the prefix to attach to the credential helper
110115
// and should be found in the $PATH. Example: ${prefix-}${helper-name}
111116
dockerAuthHelperPrefix = "docker-credential-"
117+
118+
// dockerBasicCaps is comma-separated list of Linux capabilities that are
119+
// allowed by docker by default, as documented in
120+
// https://docs.docker.com/engine/reference/run/#block-io-bandwidth-blkio-constraint
121+
dockerBasicCaps = "CHOWN,DAC_OVERRIDE,FSETID,FOWNER,MKNOD,NET_RAW,SETGID," +
122+
"SETUID,SETFCAP,SETPCAP,NET_BIND_SERVICE,SYS_CHROOT,KILL,AUDIT_WRITE"
112123
)
113124

114125
type DockerDriver struct {
@@ -202,6 +213,8 @@ type DockerDriverConfig struct {
202213
MacAddress string `mapstructure:"mac_address"` // Pin mac address to container
203214
SecurityOpt []string `mapstructure:"security_opt"` // Flags to pass directly to security-opt
204215
Devices []DockerDevice `mapstructure:"devices"` // To allow mounting USB or other serial control devices
216+
CapAdd []string `mapstructure:"cap_add"` // Flags to pass directly to cap-add
217+
CapDrop []string `mapstructure:"cap_drop"` // Flags to pass directly to cap-drop
205218
}
206219

207220
func sliceMergeUlimit(ulimitsRaw map[string]string) ([]docker.ULimit, error) {
@@ -304,6 +317,8 @@ func NewDockerDriverConfig(task *structs.Task, env *env.TaskEnv) (*DockerDriverC
304317
dconf.ExtraHosts = env.ParseAndReplace(dconf.ExtraHosts)
305318
dconf.MacAddress = env.ReplaceEnv(dconf.MacAddress)
306319
dconf.SecurityOpt = env.ParseAndReplace(dconf.SecurityOpt)
320+
dconf.CapAdd = env.ParseAndReplace(dconf.CapAdd)
321+
dconf.CapDrop = env.ParseAndReplace(dconf.CapDrop)
307322

308323
for _, m := range dconf.SysctlRaw {
309324
for k, v := range m {
@@ -644,6 +659,12 @@ func (d *DockerDriver) Validate(config map[string]interface{}) error {
644659
"devices": {
645660
Type: fields.TypeArray,
646661
},
662+
"cap_add": {
663+
Type: fields.TypeArray,
664+
},
665+
"cap_drop": {
666+
Type: fields.TypeArray,
667+
},
647668
},
648669
}
649670

@@ -1115,6 +1136,39 @@ func (d *DockerDriver) createContainerConfig(ctx *ExecContext, task *structs.Tas
11151136
}
11161137
hostConfig.Privileged = driverConfig.Privileged
11171138

1139+
// set capabilities
1140+
hostCapsWhitelistConfig := d.config.ReadDefault(
1141+
dockerCapsWhitelistConfigOption, dockerCapsWhitelistConfigDefault)
1142+
hostCapsWhitelist := make(map[string]struct{})
1143+
for _, cap := range strings.Split(hostCapsWhitelistConfig, ",") {
1144+
cap = strings.ToLower(strings.TrimSpace(cap))
1145+
hostCapsWhitelist[cap] = struct{}{}
1146+
}
1147+
1148+
if _, ok := hostCapsWhitelist["all"]; !ok {
1149+
effectiveCaps, err := tweakCapabilities(
1150+
strings.Split(dockerBasicCaps, ","),
1151+
driverConfig.CapAdd,
1152+
driverConfig.CapDrop,
1153+
)
1154+
if err != nil {
1155+
return c, err
1156+
}
1157+
var missingCaps []string
1158+
for _, cap := range effectiveCaps {
1159+
cap = strings.ToLower(cap)
1160+
if _, ok := hostCapsWhitelist[cap]; !ok {
1161+
missingCaps = append(missingCaps, cap)
1162+
}
1163+
}
1164+
if len(missingCaps) > 0 {
1165+
return c, fmt.Errorf("Docker driver doesn't have the following caps whitelisted on this Nomad agent: %s", missingCaps)
1166+
}
1167+
}
1168+
1169+
hostConfig.CapAdd = driverConfig.CapAdd
1170+
hostConfig.CapDrop = driverConfig.CapDrop
1171+
11181172
// set SHM size
11191173
if driverConfig.ShmSize != 0 {
11201174
hostConfig.ShmSize = driverConfig.ShmSize

client/driver/docker_default.go

+23-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
package driver
44

5-
import docker "github.com/fsouza/go-dockerclient"
5+
import (
6+
docker "github.com/fsouza/go-dockerclient"
7+
"github.com/moby/moby/daemon/caps"
8+
)
69

710
const (
811
// Setting default network mode for non-windows OS as bridge
@@ -12,3 +15,22 @@ const (
1215
func getPortBinding(ip string, port string) []docker.PortBinding {
1316
return []docker.PortBinding{{HostIP: ip, HostPort: port}}
1417
}
18+
19+
func tweakCapabilities(basics, adds, drops []string) ([]string, error) {
20+
// Moby mixes 2 different capabilities formats: prefixed with "CAP_"
21+
// and not. We do the conversion here to have a consistent,
22+
// non-prefixed format on the Nomad side.
23+
for i, cap := range basics {
24+
basics[i] = "CAP_" + cap
25+
}
26+
27+
effectiveCaps, err := caps.TweakCapabilities(basics, adds, drops)
28+
if err != nil {
29+
return effectiveCaps, err
30+
}
31+
32+
for i, cap := range effectiveCaps {
33+
effectiveCaps[i] = cap[len("CAP_"):]
34+
}
35+
return effectiveCaps, nil
36+
}

client/driver/docker_test.go

+124
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,130 @@ func TestDockerDriver_SecurityOpt(t *testing.T) {
10481048
}
10491049
}
10501050

1051+
func TestDockerDriver_Capabilities(t *testing.T) {
1052+
if !tu.IsTravis() {
1053+
t.Parallel()
1054+
}
1055+
if !testutil.DockerIsConnected(t) {
1056+
t.Skip("Docker not connected")
1057+
}
1058+
if runtime.GOOS == "windows" {
1059+
t.Skip("Capabilities not supported on windows")
1060+
}
1061+
1062+
testCases := []struct {
1063+
Name string
1064+
CapAdd []string
1065+
CapDrop []string
1066+
Whitelist string
1067+
StartError string
1068+
}{
1069+
{
1070+
Name: "default-whitelist-add-allowed",
1071+
CapAdd: []string{"fowner", "mknod"},
1072+
CapDrop: []string{"all"},
1073+
},
1074+
{
1075+
Name: "default-whitelist-add-forbidden",
1076+
CapAdd: []string{"net_admin"},
1077+
StartError: "net_admin",
1078+
},
1079+
{
1080+
Name: "default-whitelist-drop-existing",
1081+
CapDrop: []string{"fowner", "mknod"},
1082+
},
1083+
{
1084+
Name: "restrictive-whitelist-drop-all",
1085+
CapDrop: []string{"all"},
1086+
Whitelist: "fowner,mknod",
1087+
},
1088+
{
1089+
Name: "restrictive-whitelist-add-allowed",
1090+
CapAdd: []string{"fowner", "mknod"},
1091+
CapDrop: []string{"all"},
1092+
Whitelist: "fowner,mknod",
1093+
},
1094+
{
1095+
Name: "restrictive-whitelist-add-forbidden",
1096+
CapAdd: []string{"net_admin", "mknod"},
1097+
CapDrop: []string{"all"},
1098+
Whitelist: "fowner,mknod",
1099+
StartError: "net_admin",
1100+
},
1101+
{
1102+
Name: "permissive-whitelist",
1103+
CapAdd: []string{"net_admin", "mknod"},
1104+
Whitelist: "all",
1105+
},
1106+
{
1107+
Name: "permissive-whitelist-add-all",
1108+
CapAdd: []string{"all"},
1109+
Whitelist: "all",
1110+
},
1111+
}
1112+
1113+
for _, tc := range testCases {
1114+
t.Run(tc.Name, func(t *testing.T) {
1115+
client := newTestDockerClient(t)
1116+
task, _, _ := dockerTask(t)
1117+
if len(tc.CapAdd) > 0 {
1118+
task.Config["cap_add"] = tc.CapAdd
1119+
}
1120+
if len(tc.CapDrop) > 0 {
1121+
task.Config["cap_drop"] = tc.CapDrop
1122+
}
1123+
1124+
tctx := testDockerDriverContexts(t, task)
1125+
if tc.Whitelist != "" {
1126+
tctx.DriverCtx.config.Options[dockerCapsWhitelistConfigOption] = tc.Whitelist
1127+
}
1128+
1129+
driver := NewDockerDriver(tctx.DriverCtx)
1130+
copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar")
1131+
defer tctx.AllocDir.Destroy()
1132+
1133+
presp, err := driver.Prestart(tctx.ExecCtx, task)
1134+
defer driver.Cleanup(tctx.ExecCtx, presp.CreatedResources)
1135+
if err != nil {
1136+
t.Fatalf("Error in prestart: %v", err)
1137+
}
1138+
1139+
sresp, err := driver.Start(tctx.ExecCtx, task)
1140+
if err == nil && tc.StartError != "" {
1141+
t.Fatalf("Expected error in start: %v", tc.StartError)
1142+
} else if err != nil {
1143+
if tc.StartError == "" {
1144+
t.Fatalf("Failed to start driver: %s\nStack\n%s", err, debug.Stack())
1145+
} else if !strings.Contains(err.Error(), tc.StartError) {
1146+
t.Fatalf("Expect error containing \"%s\", got %v", tc.StartError, err)
1147+
}
1148+
return
1149+
}
1150+
1151+
if sresp.Handle == nil {
1152+
t.Fatalf("handle is nil\nStack\n%s", debug.Stack())
1153+
}
1154+
defer sresp.Handle.Kill()
1155+
handle := sresp.Handle.(*DockerHandle)
1156+
1157+
waitForExist(t, client, handle)
1158+
1159+
container, err := client.InspectContainer(handle.ContainerID())
1160+
if err != nil {
1161+
t.Fatalf("Error inspecting container: %v", err)
1162+
}
1163+
1164+
if !reflect.DeepEqual(tc.CapAdd, container.HostConfig.CapAdd) {
1165+
t.Errorf("CapAdd doesn't match.\nExpected:\n%s\nGot:\n%s\n", tc.CapAdd, container.HostConfig.CapAdd)
1166+
}
1167+
1168+
if !reflect.DeepEqual(tc.CapDrop, container.HostConfig.CapDrop) {
1169+
t.Errorf("CapDrop doesn't match.\nExpected:\n%s\nGot:\n%s\n", tc.CapDrop, container.HostConfig.CapDrop)
1170+
}
1171+
})
1172+
}
1173+
}
1174+
10511175
func TestDockerDriver_DNS(t *testing.T) {
10521176
if !tu.IsTravis() {
10531177
t.Parallel()

client/driver/docker_windows.go

+4
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ const (
1111
func getPortBinding(ip string, port string) []docker.PortBinding {
1212
return []docker.PortBinding{{HostIP: "", HostPort: port}}
1313
}
14+
15+
func tweakCapabilities(basics, adds, drops []string) ([]string, error) {
16+
return nil, nil
17+
}

0 commit comments

Comments
 (0)