Serve project SSH keys and document

Add documentation about the per-project ssh keys and serve the public
keys from the webserver.

Change-Id: I7a1aa0bd4adcecde6e1c21f29f7e0a1e88a69f98
This commit is contained in:
James E. Blair 2018-08-31 13:20:09 -07:00
parent dbe1306b36
commit 91424a6ac3
7 changed files with 83 additions and 14 deletions

View File

@ -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
<https://zuul-ci.org/docs/zuul-jobs/roles.html#role-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/<tenant>/project-ssh-key/<project>.pub`` where
``<project>`` is the canonical name of a project and ``<tenant>`` is
the name of a tenant with that project.
.. _return_values:

View File

@ -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.

1
tests/fixtures/ssh.pub vendored Normal file
View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZygeifuwUxtep5ENTPJ2sW+nmqkchtkF9o3ywnJpKJgViRTOQxbUpyfWZFVSmzaQGjarO8KMOw6w0YZ1s+cPHCtP6yw3cj7uy1ZWtxzpKH2AUW+3s74XxrUVKk78vcKQ4GFh3+vRMtn9Qrp8Fj/sT2WaeGhelnGm8HOjEYdgH/SiFkbTqjVxYFyYLrC+9qhh5fu51S5abpaXHfYM374gSvPWLCtrI1+Ws7J3jV+CfflEPex1rL17OVAmtq62fKOAl89dYxvNeA83S1ylEEKIWZVFFwjapU8d5dZFLfKE0c9ik5NcIDhahzSJjTwcCJyDzKMgadPwaKxEB++mpFkzT

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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',