Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CLI flags for Swarm Service seccomp, AppArmor, and no-new-privileges #5698

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dperny
Copy link
Contributor

@dperny dperny commented Dec 16, 2024

- What I did

Add 3 flags to the docker service create and docker service update CLI commands to support the security options in moby/moby#46386.

  • --apparmor allows setting AppArmor to default or disabled.
  • --no-new-privileges does what it says on the tin
  • --seccomp allows either default, unconfined, or a file name of a JSON file with a custom seccomp profile.

- How I did it

Added CLI flags in the standard way. Mostly boilerplate.

- How to verify it

Added tests for the flags.

- Description for the changelog

* Added `--apparmor` flag to `docker service create` and `docker service update`. Allows configuring AppArmor as `default` or `disabled`.
* Added `--no-new-privileges` flag to `docker service create` and `docker service update`.
* Added `--seccomp` flag to `docker service create` and `docker service update`. Allows setting seccomp to `default`, `unconfined`, or a custom profile.

@codecov-commenter
Copy link

codecov-commenter commented Dec 16, 2024

Codecov Report

Attention: Patch coverage is 87.36842% with 12 lines in your changes missing coverage. Please review.

Project coverage is 59.61%. Comparing base (b462778) to head (dc75d4b).
Report is 19 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5698      +/-   ##
==========================================
+ Coverage   59.51%   59.61%   +0.09%     
==========================================
  Files         346      346              
  Lines       29376    29466      +90     
==========================================
+ Hits        17483    17566      +83     
- Misses      10923    10928       +5     
- Partials      970      972       +2     

Comment on lines 376 to 381
const testJson = `{
"json": "you betcha"
}
`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the linter complains on this one;

68.89 cli/command/service/opts_test.go:376:7: ST1003: const testJson should be testJSON (stylecheck)
68.89 const testJson = `{
68.89       ^

Copy link
Member

@thaJeztah thaJeztah Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this const is only used in the test below, so perhaps consider defining the const inside that test. Give that it's a very small JSON, perhaps put it on one line?

const testJSON = `{"json": "you betcha"}`

Oh; I guess that also means changing the file to be the same , well 😂

@dperny dperny force-pushed the add-seccomp-apparmor-swarm branch 2 times, most recently from c1cec5b to b10de33 Compare December 17, 2024 13:27
@dperny
Copy link
Contributor Author

dperny commented Dec 17, 2024

What is done:

  • All CLI flags
  • Compose file parsing

What needs to be done still:

  • Error messages for bad flag values in CLI
  • Evaluate possible problems with reading seccomp JSON file with os.ReadFile
  • Compose type conversion (Compose -> Docker API types)
  • Come up with way to ingest seccomp JSON file with Compose

What I have up now is ready for review, even in its incomplete state, but not ready for merging.

@dperny dperny force-pushed the add-seccomp-apparmor-swarm branch 2 times, most recently from 2ba8aa5 to 7832807 Compare January 8, 2025 15:42
@dperny dperny marked this pull request as ready for review January 8, 2025 18:16
Comment on lines 725 to 741
// TODO(dperny): is it safe/secure to unconditionally read a file like
// this? what if the file is REALLY BIG? is that a user problem for
// passing a too-big file, or an us problem for ingesting it
// unquestioningly?
data, err := os.ReadFile(options.seccomp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it's the user's problem. The flag value is always a file path, except when it isn't; not reading a local file is the astonishing case. It's on them if the CLI process gets OOM-killed because they chose the wrong file.

That being said, interpreting any value aside from the special cases as a file path leaves no room to add new special cases, like additional seccomp modes. Adding a new special case would result in the flag --seccomp=foo changing semantics from reading a local file to whatever the special case is --- a breaking change.

I suggest that only values containing a path separator character be interpreted as a path to a custom seccomp profile. That cleanly solves several problems at once.

  • Loading a custom profile from a file is always explicit and unambiguous. Users wanting to use a profile located in the CWD would have to pass --seccomp=./my-custom-profile.json in much the same way that a shell user has to type ./my-script.sh to run an executable from the CWD.
  • All values not containing a slash or backslash character (aside from default and unconfined) are reserved for future use.

Comment on lines 731 to 744
// TODO(dperny): return this, or return "unrecognized option" or some such?
return nil, errors.Wrap(err, "unable to read seccomp custom profile file")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return this. The CLI understands the option and what the user requested. We should tell the user why reading the file failed so they can take corrective action: fix the typo in the file name, sort out file permissions, etc.

@dperny dperny force-pushed the add-seccomp-apparmor-swarm branch 3 times, most recently from a5b78ec to b66fa05 Compare January 9, 2025 17:54
@dperny dperny requested a review from a team as a code owner January 9, 2025 17:54
Adds support for some security options to the docker service create and
docker service update commands. These include:

* --no-new-privileges, which does what it says on the tin
* --seccomp, which sets seccomp options
* --apparmor, which sets apparmor options

Does not include compose file changes.

Signed-off-by: Drew Erny <[email protected]>
@dperny dperny force-pushed the add-seccomp-apparmor-swarm branch from b66fa05 to dc75d4b Compare January 13, 2025 15:43
Comment on lines +401 to +402
// why make an invalid json file when we have one lying right there?
flags.Set("seccomp", "testdata/service-context-write-raw.golden")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should re-use a golden file from another test here. We can rather create a malformed json file inside a temp directory.

fileName := filepath.Join(t.TempDir(), "malformed-json")
err := os.WriteFile(fileName, []byte("not json"), 0o644)
assert.NilError(t, err)

flags.Set("seccomp", fileName)

Comment on lines +743 to +750
// check if the file exists locally
if _, err := os.Stat(arg); errors.Is(err, os.ErrNotExist) {
return nil, errors.Errorf("unknown seccomp mode %q.", arg)
}
return nil, errors.Errorf(
"unknown seccomp mode %q. (did you mean custom a seccomp profile \"./%s\"?)",
arg, arg,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why it is necessary to check if the file exists if we expect to error anyway.

I would just remove the if statement and return
"unknown seccomp mode %q. a custom seccomp profile must be specified as a relative or absolute file path "./profile.json" or "/path/profile.json"."

}
data, err := os.ReadFile(options.seccomp)
if err != nil {
// TODO(dperny): return this, or return "unrecognized option" or some such?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot to remove?

Comment on lines +420 to +430
oldwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir("testdata"); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.Chdir(oldwd); err != nil {
panic(err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like these kind of tests that could impact all other tests from running properly. I would remove the chdir and just pass it filename. I suggested above to not check if the file exists since it's a bit superfluous. That way you won't need to chdir.

assert.Check(t, is.DeepEqual(privileges, &swarm.Privileges{
Seccomp: &swarm.SeccompOpts{
Mode: swarm.SeccompModeCustom,
Profile: []byte(testJSON),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can immediately use the json like so

Suggested change
Profile: []byte(testJSON),
Profile: []byte(`{
"json": "you betcha",
}`),

Comment on lines +378 to +381
const testJSON = `{
"json": "you betcha"
}
`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just remove this const.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants