diff --git a/doc/source/user/jobs.rst b/doc/source/user/jobs.rst index ebfab496a3..84014b7441 100644 --- a/doc/source/user/jobs.rst +++ b/doc/source/user/jobs.rst @@ -597,17 +597,46 @@ executor running the job is available: SSH Keys -------- -Zuul starts each job with an SSH agent running and the key used to -access the job's nodes added to that agent. Generally you won't need -to be aware of this since Ansible will use this when performing any -tasks on remote nodes. However, under some circumstances you may want -to interact with the agent. For example, you may wish to add a key -provided as a secret to the job in order to access a specific host, or -you may want to, in a pre-playbook, replace the key used to log into -the assigned nodes in order to further protect it from being abused by -untrusted job content. +Zuul starts each job with an SSH agent running and at least one key +added to that agent. Generally you won't need to be aware of this +since Ansible will use this when performing any tasks on remote nodes. +However, under some circumstances you may want to interact with the +agent. For example, you may wish to add a key provided as a secret to +the job in order to access a specific host, or you may want to, in a +pre-playbook, replace the key used to log into the assigned nodes in +order to further protect it from being abused by untrusted job +content. -.. TODO: describe standard lib and link to published docs for it. +A description of each of the keys added to the SSH agent follows. + +Nodepool Key +~~~~~~~~~~~~ + +This key is supplied by the system administrator. It is expected to +be accepted by every node supplied by Nodepool and is generally the +key that will be used by Zuul when running jobs. Because of the +potential for an unrelated job to add an arbitrary host to the Ansible +inventory which might accept this key (e.g., a node for another job, +or a static host), the use of the `add-build-sshkey +` +role is recommended. + +Project Key +~~~~~~~~~~~ + +Each project in Zuul has its own SSH keypair. This key is added to +the SSH agent for all jobs running in a post-review pipeline. If a +system administrator trusts that project, they can add the project's +public key to systems to allow post-review jobs to access those +systems. The systems may be added to the inventory using the +``add_host`` Ansible module, or they may be supplied by static nodes +in Nodepool. + +Zuul serves each project's public SSH key using its build-in +webserver. They can be fetched at the path +``/api/tenant//project-ssh-key/.pub`` where +```` is the canonical name of a project and ```` is +the name of a tenant with that project. .. _return_values: diff --git a/releasenotes/notes/project_key-d9bd1f25b7d39384.yaml b/releasenotes/notes/project_key-d9bd1f25b7d39384.yaml new file mode 100644 index 0000000000..617fd70b6c --- /dev/null +++ b/releasenotes/notes/project_key-d9bd1f25b7d39384.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + An SSH keypair is now generated for every project and may be used in + post-review jobs to access systems which trust that project. diff --git a/tests/fixtures/ssh.pub b/tests/fixtures/ssh.pub new file mode 100644 index 0000000000..435339da79 --- /dev/null +++ b/tests/fixtures/ssh.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZygeifuwUxtep5ENTPJ2sW+nmqkchtkF9o3ywnJpKJgViRTOQxbUpyfWZFVSmzaQGjarO8KMOw6w0YZ1s+cPHCtP6yw3cj7uy1ZWtxzpKH2AUW+3s74XxrUVKk78vcKQ4GFh3+vRMtn9Qrp8Fj/sT2WaeGhelnGm8HOjEYdgH/SiFkbTqjVxYFyYLrC+9qhh5fu51S5abpaXHfYM374gSvPWLCtrI1+Ws7J3jV+CfflEPex1rL17OVAmtq62fKOAl89dYxvNeA83S1ylEEKIWZVFFwjapU8d5dZFLfKE0c9ik5NcIDhahzSJjTwcCJyDzKMgadPwaKxEB++mpFkzT diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 2aaccc7e0b..64aa2a1466 100755 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -385,6 +385,14 @@ class TestWeb(BaseTestWeb): resp = self.get_url("api/tenant/tenant-one/key/org/no-project.pub") self.assertEqual(404, resp.status_code) + with open(os.path.join(FIXTURE_DIR, 'ssh.pub'), 'rb') as f: + public_ssh = f.read() + + resp = self.get_url("api/tenant/tenant-one/project-ssh-key/" + "org/project.pub") + self.assertEqual(resp.content, public_ssh) + self.assertIn('text/plain', resp.headers.get('Content-Type')) + def test_web_404_on_unknown_tenant(self): resp = self.get_url("api/tenant/non-tenant/status") self.assertEqual(404, resp.status_code) diff --git a/zuul/lib/keystorage.py b/zuul/lib/keystorage.py index 852d8d68ac..fd8abe07fd 100644 --- a/zuul/lib/keystorage.py +++ b/zuul/lib/keystorage.py @@ -167,7 +167,7 @@ class KeyStorage(object): with open(private_key_file, 'r') as f: private_key = f.read() public_key = key.get_base64() - return (private_key, public_key) + return (private_key, 'ssh-rsa ' + public_key) def _createSSHKey(self, fn): key_dir = os.path.dirname(fn) diff --git a/zuul/rpclistener.py b/zuul/rpclistener.py index e10280c307..9c0a4e977f 100644 --- a/zuul/rpclistener.py +++ b/zuul/rpclistener.py @@ -399,8 +399,16 @@ class RPCListener(object): if not project: job.sendWorkComplete("") return - job.sendWorkComplete( - encryption.serialize_rsa_public_key(project.public_secrets_key)) + keytype = args.get('key', 'secrets') + if keytype == 'secrets': + job.sendWorkComplete( + encryption.serialize_rsa_public_key( + project.public_secrets_key)) + elif keytype == 'ssh': + job.sendWorkComplete(project.public_ssh_key) + else: + job.sendWorkComplete("") + return def handle_config_errors_list(self, job): args = json.loads(job.arguments) diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index 4f267c49c5..8b0efbe0a4 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -305,7 +305,8 @@ class ZuulWebAPI(object): @cherrypy.tools.save_params() def key(self, tenant, project): job = self.rpc.submitJob('zuul:key_get', {'tenant': tenant, - 'project': project}) + 'project': project, + 'key': 'secrets'}) if not job.data: raise cherrypy.HTTPError( 404, 'Project %s does not exist.' % project) @@ -314,6 +315,20 @@ class ZuulWebAPI(object): resp.headers['Content-Type'] = 'text/plain' return job.data[0] + @cherrypy.expose + @cherrypy.tools.save_params() + def project_ssh_key(self, tenant, project): + job = self.rpc.submitJob('zuul:key_get', {'tenant': tenant, + 'project': project, + 'key': 'ssh'}) + if not job.data: + raise cherrypy.HTTPError( + 404, 'Project %s does not exist.' % project) + resp = cherrypy.response + resp.headers['Access-Control-Allow-Origin'] = '*' + resp.headers['Content-Type'] = 'text/plain' + return job.data[0] + '\n' + @cherrypy.expose @cherrypy.tools.save_params() @cherrypy.tools.json_out(content_type='application/json; charset=utf-8') @@ -488,6 +503,9 @@ class ZuulWeb(object): controller=api, action='job') route_map.connect('api', '/api/tenant/{tenant}/key/{project:.*}.pub', controller=api, action='key') + route_map.connect('api', '/api/tenant/{tenant}/' + 'project-ssh-key/{project:.*}.pub', + controller=api, action='project_ssh_key') route_map.connect('api', '/api/tenant/{tenant}/console-stream', controller=api, action='console_stream') route_map.connect('api', '/api/tenant/{tenant}/builds',