hashicorp/nomad · error

invalid security_opt: %q

Error message

invalid security_opt: %q

What it means

parseSecurityOpts validates the task's security_opt entries before passing them to the Docker daemon. Each option must be either key=value, the bare token 'no-new-privileges', or contain a ':' separator. An option in none of those forms is rejected here so the daemon never sees malformed security options.

Source

Thrown at drivers/docker/driver.go:914

	// Empty string maps to `rprivate` for backwards compatibility in restored
	// older tasks, where mount propagation will not be present.
	"":                                     "rprivate",
	nstructs.VolumeMountPropagationPrivate: "rprivate",
	nstructs.VolumeMountPropagationHostToTask:    "rslave",
	nstructs.VolumeMountPropagationBidirectional: "rshared",
}

// takes a local seccomp daemon, reads the file contents for sending to the daemon
// this code modified slightly from the docker CLI code
// https://github.com/docker/cli/blob/8ef8547eb6934b28497d309d21e280bcd25145f5/cli/command/container/opts.go#L840
func parseSecurityOpts(securityOpts []string) ([]string, error) {
	for key, opt := range securityOpts {
		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 securityOpts, fmt.Errorf("invalid security_opt: %q", opt)
			}
		}
		if con[0] == "seccomp" && con[1] != "unconfined" {
			f, err := os.ReadFile(con[1])
			if err != nil {
				return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %v", con[1], err)
			}
			b := bytes.NewBuffer(nil)
			if err := json.Compact(b, f); err != nil {
				return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err)
			}
			securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
		}
	}

	return securityOpts, nil
}

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Use key=value form, e.g. "seccomp=unconfined" or "apparmor=docker-default".
  2. Use colon form where applicable, e.g. "label:disable".
  3. For privilege escalation control, use the exact token "no-new-privileges".
  4. Remove options that carry no value if they are not needed.

Example fix

// before
config { security_opt = ["seccomp"] }
// after
config { security_opt = ["seccomp=unconfined"] }
Defensive patterns

Strategy: validation

Validate before calling

func validSecurityOpt(opt string) bool {
	if opt == "no-new-privileges" { return true }
	return strings.Contains(opt, "=") || strings.Contains(opt, ":")
}

Prevention

When it happens

Trigger: config.security_opt contains a bare token without '=' or ':' (other than no-new-privileges), e.g. "seccomp" alone, "apparmor", or a misspelled flag like "no new privileges". Raised in createContainerConfig.

Common situations: Hand-writing security opts copied from docker CLI docs incorrectly (docker accepts some bare flags the parser here doesn't), forgetting the '=profile' part of seccomp=..., or whitespace mangling in HCL arrays.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/8be7e042ab41fe9f. Report an issue: GitHub.