Bug 1487500: improvements to create-interactive r=bstack
authorDustin J. Mitchell <dustin@mozilla.com>
Mon, 17 Sep 2018 23:28:39 +0000
changeset 481181 3043bc2a9bbfc8916b3b567ea1ef37ad4e454c9d
parent 481180 23c987fff84331e89577d0d6d860fc98f89cacf5
child 481182 04225268cddf07f2864d407560bbd53bb36629e4
push id1799
push usermozilla@hocat.ca
push dateFri, 21 Sep 2018 21:40:19 +0000
treeherdermozilla-release@aa7b44bb25aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbstack
bugs1487500
milestone62.0.3
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 = {