diff --git a/.taskcluster.yml b/.taskcluster.yml index bc2b2d31..c1594e67 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -15,6 +15,7 @@ tasks: scopes: - 'docker-worker:cache:docker-worker-garbage-*' - 'docker-worker:capability:privileged' + - 'docker-worker:capability:disableSeccomp' - 'docker-worker:capability:device:loopbackAudio' - 'docker-worker:capability:device:loopbackVideo' - 'secrets:get:project/taskcluster/testing/docker-worker/ci-creds' @@ -28,6 +29,7 @@ tasks: while ! yarn install --frozen-lockfile; do rm -rf node_modules; sleep 30; done && node_modules/.bin/eslint src test deploy capabilities: privileged: true + disableSeccomp: true devices: loopbackAudio: true loopbackVideo: true @@ -57,6 +59,7 @@ tasks: scopes: - 'docker-worker:cache:docker-worker-garbage-*' - 'docker-worker:capability:privileged' + - 'docker-worker:capability:disableSeccomp' - 'docker-worker:capability:device:loopbackAudio' - 'docker-worker:capability:device:loopbackVideo' - 'secrets:get:project/taskcluster/testing/docker-worker/ci-creds' @@ -71,6 +74,7 @@ tasks: ./test/docker-worker-test --this-chunk 1 --total-chunks 5 capabilities: privileged: true + disableSeccomp: true devices: loopbackAudio: true loopbackVideo: true @@ -100,6 +104,7 @@ tasks: scopes: - 'docker-worker:cache:docker-worker-garbage-*' - 'docker-worker:capability:privileged' + - 'docker-worker:capability:disableSeccomp' - 'docker-worker:capability:device:loopbackAudio' - 'docker-worker:capability:device:loopbackVideo' - 'secrets:get:project/taskcluster/testing/docker-worker/ci-creds' @@ -114,6 +119,7 @@ tasks: ./test/docker-worker-test --this-chunk 2 --total-chunks 5 capabilities: privileged: true + disableSeccomp: true devices: loopbackAudio: true loopbackVideo: true @@ -143,6 +149,7 @@ tasks: scopes: - 'docker-worker:cache:docker-worker-garbage-*' - 'docker-worker:capability:privileged' + - 'docker-worker:capability:disableSeccomp' - 'docker-worker:capability:device:loopbackAudio' - 'docker-worker:capability:device:loopbackVideo' - 'secrets:get:project/taskcluster/testing/docker-worker/ci-creds' @@ -157,6 +164,7 @@ tasks: ./test/docker-worker-test --this-chunk 3 --total-chunks 5 capabilities: privileged: true + disableSeccomp: true devices: loopbackAudio: true loopbackVideo: true @@ -186,6 +194,7 @@ tasks: scopes: - 'docker-worker:cache:docker-worker-garbage-*' - 'docker-worker:capability:privileged' + - 'docker-worker:capability:disableSeccomp' - 'docker-worker:capability:device:loopbackAudio' - 'docker-worker:capability:device:loopbackVideo' - 'secrets:get:project/taskcluster/testing/docker-worker/ci-creds' @@ -200,6 +209,7 @@ tasks: ./test/docker-worker-test --this-chunk 4 --total-chunks 5 capabilities: privileged: true + disableSeccomp: true devices: loopbackAudio: true loopbackVideo: true @@ -229,6 +239,7 @@ tasks: scopes: - 'docker-worker:cache:docker-worker-garbage-*' - 'docker-worker:capability:privileged' + - 'docker-worker:capability:disableSeccomp' - 'docker-worker:capability:device:loopbackAudio' - 'docker-worker:capability:device:loopbackVideo' - 'secrets:get:project/taskcluster/testing/docker-worker/ci-creds' @@ -243,6 +254,7 @@ tasks: ./test/docker-worker-test --this-chunk 5 --total-chunks 5 capabilities: privileged: true + disableSeccomp: true devices: loopbackAudio: true loopbackVideo: true diff --git a/config.yml b/config.yml index 403e80bb..10c5f0b3 100644 --- a/config.yml +++ b/config.yml @@ -29,6 +29,10 @@ defaults: delayFactor: 15000 # Value between 0 and 1 randomizationFactor: 0.25 + # Disable the seccomp system call filter. This is useful for some tasks + # (eg. using `rr`) but it allows significant information leakage, and its + # use should not be considered secure. + disableSeccomp: false features: relengAPIProxy: image: 'taskcluster/relengapi-proxy:2.3.1' @@ -192,6 +196,7 @@ test: maxAttempts: 5 delayFactor: 100 randomizationFactor: 0.25 + disableSeccomp: false taskQueue: pollInterval: 1000 diff --git a/schemas/v1/payload.json b/schemas/v1/payload.json index b16782be..f2975624 100644 --- a/schemas/v1/payload.json +++ b/schemas/v1/payload.json @@ -132,6 +132,12 @@ "type": "boolean", "default": false }, + "disableSeccomp": { + "title": "Container does not have a seccomp profile set.", + "description": "Allows a task to run without seccomp, similar to running docker with `--security-opt seccomp=unconfined`. This only works for worker-types configured to enable it.", + "type": "boolean", + "default": false + }, "devices": { "title": "Devices to be attached to task containers", "description": "Allows devices from the host system to be attached to a task container similar to using `--device` in docker. ", diff --git a/src/lib/host/packet.js b/src/lib/host/packet.js index b5b00133..d8103ef7 100644 --- a/src/lib/host/packet.js +++ b/src/lib/host/packet.js @@ -42,6 +42,7 @@ module.exports = { // * worker type - the taskcluster worker type name // * capacity - the worker capacity // * allowPrivileged - boolean indicating if the instance is allowed to run privileged docker containers + // * disableSeccomp - boolean indicating if the instance is allowed to run without seccomp sandbox const userdata = fs.readFileSync('/var/lib/cloud/instance/user-data.txt') .toString() @@ -95,6 +96,7 @@ module.exports = { }, dockerConfig: { allowPrivileged: userdata.allowPrivileged == 'true', + disableSeccomp: userdata.disableSeccomp == 'true', }, logging: { secureLiveLogging: false, diff --git a/src/lib/task.js b/src/lib/task.js index b30ffe3c..1ec4b4e0 100644 --- a/src/lib/task.js +++ b/src/lib/task.js @@ -128,6 +128,28 @@ function runAsPrivileged(task, allowPrivilegedTasks) { return true; } +function runWithoutSeccomp(task, allowDisableSeccompTasks) { + let taskCapabilities = task.payload.capabilities || {}; + let disableSeccompTask = taskCapabilities.disableSeccomp || false; + if (!disableSeccompTask) return false; + + if (!scopeMatch(task.scopes, [['docker-worker:capability:disableSeccomp']])) { + throw new Error( + 'Insufficient scopes to run task without seccomp. Try ' + + 'adding docker-worker:capability:disableSeccomp to the .scopes array' + ); + } + + if (!allowDisableSeccompTasks) { + throw new Error( + 'Cannot run task using docker without a seccomp profile. Worker ' + + 'must be enabled to allow running of tasks without seccomp.' + ); + } + + return true; +} + async function buildDeviceBindings(devices, expandedScopes) { let allowed = await hasPrefixedScopes('docker-worker:capability:device:', devices, expandedScopes); @@ -328,6 +350,10 @@ class Task extends EventEmitter { this.task, this.runtime.dockerConfig.allowPrivileged ); + let disableSeccompTask = runWithoutSeccomp( + this.task, this.runtime.dockerConfig.allowDisableSeccomp + ); + let procConfig = { start: {}, create: { @@ -351,6 +377,9 @@ class Task extends EventEmitter { } } }; + if (disableSeccompTask) { + procConfig.create.HostConfig.SecurityOpt = ['seccomp=unconfined']; + } // Zero is a valid option so only check for existence. if ('cpusetCpus' in this.options) { diff --git a/test/integration/disable_seccomp_containers_test.js b/test/integration/disable_seccomp_containers_test.js new file mode 100644 index 00000000..102997af --- /dev/null +++ b/test/integration/disable_seccomp_containers_test.js @@ -0,0 +1,113 @@ +const assert = require('assert'); +const settings = require('../settings'); +const DockerWorker = require('../dockerworker'); +const TestWorker = require('../testworker'); + +suite('disableSeccomp capability', () => { + let worker; + + setup(async () => { + settings.cleanup(); + }); + + teardown(async () => { + settings.cleanup(); + if (worker) await worker.terminate(); + worker = null; + }); + + test('task error when necessary scopes missing', async () => { + settings.configure({ + dockerConfig: { + disableSeccomp: true + } + }); + + worker = new TestWorker(DockerWorker); + await worker.launch(); + let result = await worker.postToQueue({ + payload: { + image: 'taskcluster/test-ubuntu', + command: [ + '/bin/bash', + '-c', + 'sleep 1' + ], + capabilities: { + disableSeccomp: true + }, + maxRunTime: 5 * 60 + } + }); + + let errorMessage = 'Insufficient scopes to run task without seccomp'; + assert.ok(result.log.indexOf(errorMessage) !== -1); + assert.equal(result.run.state, 'failed', 'task should not be successful'); + assert.equal(result.run.reasonResolved, 'failed', 'task should not be successful'); + }); + + test('task error when disableSeccomp requested but not enabled in worker', async () => { + worker = new TestWorker(DockerWorker); + await worker.launch(); + let result = await worker.postToQueue({ + scopes: ['docker-worker:capability:disableSeccomp'], + payload: { + image: 'taskcluster/test-ubuntu', + command: [ + '/bin/bash', + '-c', + 'sleep 1' + ], + capabilities: { + disableSeccomp: true + }, + maxRunTime: 5 * 60 + } + }); + + let errorMessage = 'Error: Cannot run task using docker without a seccomp profile'; + assert.ok(result.log.indexOf(errorMessage) !== -1); + assert.equal(result.run.state, 'failed', 'task should not be successful'); + assert.equal(result.run.reasonResolved, 'failed', 'task should not be successful'); + }); + + test('use performance counter in a container without disableSeccomp -- task should fail', async () => { + worker = new TestWorker(DockerWorker); + await worker.launch(); + let result = await worker.postToQueue({ + payload: { + image: 'alpine', + command: ['/bin/sh', '-c', 'echo http://dl-cdn.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories; apk add perf; perf stat ls'], + maxRunTime: 5 * 60 + } + }); + + assert(result.run.state === 'failed', 'task should fail'); + assert(result.run.reasonResolved === 'failed', 'task should fail'); + }); + + test('use performance counter in a container with disableSeccomp -- task should succeed', async () => { + settings.configure({ + dockerConfig: { + allowPrivileged: true + } + }); + + worker = new TestWorker(DockerWorker); + await worker.launch(); + let result = await worker.postToQueue({ + scopes: ['docker-worker:capability:disableSeccomp'], + payload: { + image: 'alpine', + command: ['/bin/sh', '-c', 'echo http://dl-cdn.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories; apk add perf; perf stat ls'], + capabilities: { + disableSeccomp: true, + }, + maxRunTime: 5 * 60 + } + }); + + assert(result.run.state === 'completed', 'task should not fail'); + assert(result.run.reasonResolved === 'completed', 'task should not fail'); + }); +});