diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b26dd5..7932174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ IMPROVEMENTS: - * config: Add `socket` stanza to allow multiple Podman sockets to be used. [[GH-371](https://github.com/hashicorp/nomad-driver-podman/pull/371)] + +* config: Allow setting `security_opt` option. [[GH-382](https://github.com/hashicorp/nomad-driver-podman/pull/382)] +* config: Add `socket` stanza to allow multiple Podman sockets to be used. [[GH-371](https://github.com/hashicorp/nomad-driver-podman/pull/371)] ## 0.6.1 (July 15, 2024) diff --git a/README.md b/README.md index 5858f5b..93a7891 100644 --- a/README.md +++ b/README.md @@ -451,6 +451,16 @@ config { } ``` +* **security_opt** - (Optional) A list of security-related options that are set in the container. + +```hcl +config { + security_opt = [ + "no-new-privileges" + ] +} +``` + * **selinux_opts** - (Optional) A list of process labels the container will use. ``` diff --git a/api/structs.go b/api/structs.go index f8fa929..0b8d24d 100644 --- a/api/structs.go +++ b/api/structs.go @@ -57,6 +57,10 @@ type ContainerBasicConfig struct { // container. // Optional. Env map[string]string `json:"env,omitempty"` + // Labels nested indicates whether or not the container is allowed + // to run fully nested containers including SELinux labelling. + // Optional. + LabelsNested bool `json:"labels_nested,omitempty"` // Labels are key-value pairs that are used to add metadata to // containers. // Optional. @@ -298,7 +302,15 @@ type ContainerSecurityConfig struct { // privileges flag on create, which disables gaining additional // privileges (e.g. via setuid) in the container. NoNewPrivileges bool `json:"no_new_privileges,omitempty"` - + // Mask is the path we want to mask in the container. This masks the paths + // given in addition to the default list. + // Optional + Mask []string `json:"mask,omitempty"` + // Unmask a path in the container. Some paths are masked by default, + // preventing them from being accessed within the container; this undoes that masking. + // If ALL is passed, all paths will be unmasked. + // Optional. + Unmask []string `json:"unmask,omitempty"` // IDMappings are UID and GID mappings that will be used by user // namespaces. // Required if UserNS is private. diff --git a/config.go b/config.go index d104e32..b5ed15f 100644 --- a/config.go +++ b/config.go @@ -117,6 +117,7 @@ var ( "port_map": hclspec.NewAttr("port_map", "list(map(number))", false), "ports": hclspec.NewAttr("ports", "list(string)", false), "privileged": hclspec.NewAttr("privileged", "bool", false), + "security_opt": hclspec.NewAttr("security_opt", "list(string)", false), "socket": hclspec.NewDefault( hclspec.NewAttr("socket", "string", false), hclspec.NewLiteral(`"default"`), @@ -130,6 +131,7 @@ var ( "readonly_rootfs": hclspec.NewAttr("readonly_rootfs", "bool", false), "userns": hclspec.NewAttr("userns", "string", false), "shm_size": hclspec.NewAttr("shm_size", "string", false), + }) ) @@ -244,4 +246,5 @@ type TaskConfig struct { ReadOnlyRootfs bool `codec:"readonly_rootfs"` UserNS string `codec:"userns"` ShmSize string `codec:"shm_size"` + SecurityOpt []string `codec:"security_opt"` } diff --git a/driver.go b/driver.go index a6598a9..ea54b61 100644 --- a/driver.go +++ b/driver.go @@ -728,6 +728,11 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive createOpts.ContainerSecurityConfig.ReadOnlyFilesystem = podmanTaskConfig.ReadOnlyRootfs createOpts.ContainerSecurityConfig.ApparmorProfile = podmanTaskConfig.ApparmorProfile + // add security_opt if configured + if securiyOptsErr := parseSecurityOpt(podmanTaskConfig.SecurityOpt, &createOpts); securiyOptsErr != nil { + return nil, nil, fmt.Errorf("failed to parse security_opt configuration: %v", securiyOptsErr) + } + // Populate --userns mode only if configured if podmanTaskConfig.UserNS != "" { userns := strings.SplitN(podmanTaskConfig.UserNS, ":", 2) @@ -1648,3 +1653,55 @@ func setExtraHosts(hosts []string, createOpts *api.SpecGenerator) error { createOpts.ContainerNetworkConfig.HostAdd = slices.Clone(hosts) return nil } + +func parseSecurityOpt(securityOpt []string, createOpts *api.SpecGenerator) error { + createOpts.Annotations = make(map[string]string) + for _, opt := range securityOpt { + con := strings.SplitN(opt, "=", 2) + if len(con) == 1 && con[0] != "no-new-privileges" { + if strings.Contains(opt, ":") { + con = strings.SplitN(opt, ":", 2) + } else { + return fmt.Errorf("invalid security_opt: %q", opt) + } + } + + switch con[0] { + case "no-new-privileges": + createOpts.ContainerSecurityConfig.NoNewPrivileges = true + continue + case "label": + if con[1] == "nested" { + createOpts.ContainerBasicConfig.LabelsNested = true + continue + } + createOpts.ContainerSecurityConfig.SelinuxOpts = append(createOpts.ContainerSecurityConfig.SelinuxOpts, con[1]) + createOpts.Annotations["io.podman.annotations.label"] = strings.Join(createOpts.ContainerSecurityConfig.SelinuxOpts, ",label=") + case "apparmor": + createOpts.ContainerSecurityConfig.ApparmorProfile = con[1] + createOpts.Annotations["io.podman.annotations.apparmor"] = con[1] + case "seccomp": + createOpts.ContainerSecurityConfig.SeccompProfilePath = con[1] + createOpts.Annotations["io.podman.annotations.seccomp"] = con[1] + case "proc-opts": + createOpts.ContainerSecurityConfig.ProcOpts = strings.Split(con[1], ",") + case "mask": + createOpts.ContainerSecurityConfig.Mask = append(createOpts.ContainerSecurityConfig.Mask, strings.Split(con[1], ":")...) + case "unmask": + if con[1] == "ALL" { + createOpts.ContainerSecurityConfig.Unmask = append(createOpts.ContainerSecurityConfig.Unmask, con[1]) + continue + } + createOpts.ContainerSecurityConfig.Unmask = append(createOpts.ContainerSecurityConfig.Unmask, strings.Split(con[1], ":")...) + case "systempaths": + if con[1] == "unconfined" { + createOpts.ContainerSecurityConfig.Unmask = append(createOpts.ContainerSecurityConfig.Unmask, []string{"ALL"}...) + } else { + return fmt.Errorf("invalid systempaths option %q, only `unconfined` is supported", con[1]) + } + default: + return fmt.Errorf("invalid security_opt: %q", opt) + } + } + return nil +} diff --git a/driver_test.go b/driver_test.go index 5f41673..f56c380 100644 --- a/driver_test.go +++ b/driver_test.go @@ -1501,6 +1501,16 @@ func TestPodmanDriver_Caps(t *testing.T) { } } +// check security_opt option +func TestPodmanDriver_SecurityOpt(t *testing.T) { + taskCfg := newTaskConfig("", busyboxLongRunningCmd) + // add a security_opt + taskCfg.SecurityOpt = []string{"no-new-privileges"} + inspectData := startDestroyInspect(t, taskCfg, "securityopt") + // and compare it + must.SliceContains(t, inspectData.HostConfig.SecurityOpt, "no-new-privileges") +} + // check enabled tty option func TestPodmanDriver_Tty(t *testing.T) { ci.Parallel(t)