Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions lib/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,34 @@ function normalizeService(
);
}

// Reject all security_opt settings except no-new-privileges
if (service.security_opt?.some((opt) => !opt.match('no-new-privileges'))) {
// Reject all security_opt settings except no-new-privileges, unconfined apparmor/seccomp.
// compose-go preserves the original separator, which can be `:` or `=`, so accept both.
// Note: `systempaths=unconfined` is intentionally not allowed, as balenaEngine
// does not handle it in parseSecurityOpt and rejects container creation.
const allowedSecurityOpts = [
/^no-new-privileges([:=].*)?$/,
/^apparmor[:=]unconfined$/,
/^seccomp[:=]unconfined$/,
];
if (
service.security_opt?.some(
(opt) => !allowedSecurityOpts.some((re) => re.test(opt)),
)
) {
throw new ServiceError(
'Only no-new-privileges is allowed for service.security_opt',
'Only no-new-privileges and unconfined apparmor and seccomp are allowed for service.security_opt',
serviceName,
);
}

// Normalize the security_opt separator to `=`. Moby and balenaEngine parse
// `key=value`, and treat `key:value` as accepted but deprecated.
if (service.security_opt) {
service.security_opt = service.security_opt.map((opt) =>
opt.replace(':', '='),
);
}

// Reject volumes_from which references container:${containerId}
if (service.volumes_from?.some((v) => v.match(/^container:.*$/))) {
throw new ServiceError(
Expand Down
17 changes: 14 additions & 3 deletions test/compose.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1021,23 +1021,34 @@ describe('compose-go parsing & validation', () => {
});
});

it('should reject all security_opt settings except no-new-privileges', async () => {
it('should reject unsupported security_opt settings', async () => {
try {
await parse(
'test/fixtures/compose/services/unsupported/security_opt_unsupported.yml',
);
expect.fail(
'Expected compose parser to reject security_opt settings except no-new-privileges',
'Expected compose parser to reject unsupported security_opt settings',
);
} catch (error) {
expect(error).to.be.instanceOf(ServiceError);
expect(error.serviceName).to.equal('main');
expect(error.message).to.equal(
'Only no-new-privileges is allowed for service.security_opt',
'Only no-new-privileges and unconfined apparmor and seccomp are allowed for service.security_opt',
);
}
});

it('should allow no-new-privileges and unconfined apparmor and seccomp in security_opt, normalizing the separator to `=`', async () => {
const composition = await parse(
'test/fixtures/compose/services/security_opt_supported.yml',
);
expect(composition.services.main.security_opt).to.deep.equal([
'no-new-privileges=true',
'apparmor=unconfined',
'seccomp=unconfined',
]);
});

it('should reject volumes of type bind', async () => {
try {
await parse(
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/compose/services/security_opt_supported.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
main:
image: alpine:latest
command: sh -c "sleep infinity"
security_opt:
- no-new-privileges:true
- apparmor=unconfined
- seccomp:unconfined
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ services:
- label:role:balena
- label:disable
- apparmor:MY_PROFILE
- seccomp:unconfined
- seccomp:builtin
- seccomp:profile.json
- systempaths:unconfined
- systempaths:unconfined
Loading