Bug 1487500: improvements to create-interactive r=bstack
authorDustin J. Mitchell <dustin@mozilla.com>
Mon, 17 Sep 2018 23:28:39 +0000
changeset 436908 d601a7868c0469c546de607886294651ac5725d6
parent 436907 b444a3e482d851a920442aa10a56afd8f02540d3
child 436909 bdfd4802db0cfb656dbb001676fdbe9766112b30
push id34665
push userebalazs@mozilla.com
push dateTue, 18 Sep 2018 14:32:15 +0000
treeherdermozilla-central@d073607e0ffa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbstack
bugs1487500
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1487500: improvements to create-interactive r=bstack Some whitelisting of scopes, and some notes on security of the operation. Differential Revision: https://phabricator.services.mozilla.com/D5572
taskcluster/taskgraph/actions/create_interactive.py
--- a/taskcluster/taskgraph/actions/create_interactive.py
+++ b/taskcluster/taskgraph/actions/create_interactive.py
@@ -2,16 +2,17 @@
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import logging
+import re
 
 from .util import (
     create_tasks,
     fetch_graph_and_labels
 )
 from taskgraph.util.taskcluster import send_email
 from .registry import register_callback_action
 
@@ -19,28 +20,60 @@ logger = logging.getLogger(__name__)
 
 EMAIL_SUBJECT = 'Your Interactive Task for {label}'
 EMAIL_CONTENT = '''\
 As you requested, Firefox CI has created an interactive task to run {label}
 on revision {revision} in {repo}. Click the button below to connect to the
 task. You may need to wait for it to begin running.
 '''
 
+###
+# Security Concerns
+#
+# An "interactive task" is, quite literally, shell access to a worker. That
+# is limited by being in a Docker container, but we assume that Docker has
+# bugs so we do not want to rely on container isolation exclusively.
+#
+# Interactive tasks should never be allowed on hosts that build binaries
+# leading to a release -- level 3 builders.
+#
+# Users must not be allowed to create interactive tasks for tasks above
+# their own level.
+#
+# Interactive tasks must not have any routes that might make them appear
+# in the index to be used by other production tasks.
+#
+# Interactive tasks should not be able to write to any docker-worker caches.
+
+SCOPE_WHITELIST = [
+    # this is not actually secret, and just about everything needs it
+    re.compile(r'^secrets:get:project/taskcluster/gecko/hgfingerprint$'),
+    # public downloads are OK
+    re.compile(r'^docker-worker:relengapi-proxy:tooltool.download.public$'),
+    # level-appropriate secrets are generally necessary to run a task; these
+    # also are "not that secret" - most of them are built into the resulting
+    # binary and could be extracted by someone with `strings`.
+    re.compile(r'^secrets:get:project/releng/gecko/build/level-[0-9]/\*'),
+]
+
 
 @register_callback_action(
     title='Create Interactive Task',
     name='create-interactive',
     symbol='create-inter',
     kind='hook',
     generic=True,
     description=(
         'Create a a copy of the task that you can interact with'
     ),
     order=50,
     context=[{'worker-implementation': 'docker-worker'}],
+    # only available on level 1, 2 runs
+    # TODO: support tests on level 3
+    available=lambda params: int(params['level']) < 3,
     schema={
         'type': 'object',
         'properties': {
             'notify': {
                 'type': 'string',
                 'format': 'email',
                 'title': 'Who to notify of the pending interactive task',
                 'description': (
@@ -72,39 +105,43 @@ def create_interactive_action(parameters
 
         # only try this once
         task_def['retries'] = 0
 
         # short expirations, at least 3 hour maxRunTime
         task_def['deadline'] = {'relative-datestamp': '12 hours'}
         task_def['created'] = {'relative-datestamp': '0 hours'}
         task_def['expires'] = {'relative-datestamp': '1 day'}
+
+        # filter scopes with the SCOPE_WHITELIST
+        task.task['scopes'] = [s for s in task.task.get('scopes', [])
+                               if any(p.match(s) for p in SCOPE_WHITELIST)]
+
         payload = task_def['payload']
+
+        # make sure the task runs for long enough..
         payload['maxRunTime'] = max(3600 * 3, payload.get('maxRunTime', 0))
 
-        # no caches
-        task_def['scopes'] = [s for s in task_def['scopes']
-                              if not s.startswith('docker-worker:cache:')]
+        # no caches or artifacts
         payload['cache'] = {}
-
-        # no artifacts
         payload['artifacts'] = {}
 
         # enable interactive mode
         payload.setdefault('features', {})['interactive'] = True
         payload.setdefault('env', {})['TASKCLUSTER_INTERACTIVE'] = 'true'
 
         return task
 
     # Create the task and any of its dependencies. This uses a new taskGroupId to avoid
     # polluting the existing taskGroup with interactive tasks.
     label_to_taskid = create_tasks([label], full_task_graph, label_to_taskid,
                                    parameters, modifier=edit)
 
     taskId = label_to_taskid[label]
+    logger.info('Created interactive task {}; sending notification'.format(taskId))
 
     if input and 'notify' in input:
         email = input['notify']
         # no point sending to a noreply address!
         if email == 'noreply@noreply.mozilla.org':
             return
 
         info = {