Create per-project ssh keys

This creates and loads the keys, but does not supply them to the
executor yet.

Change-Id: I74e7c21bdf5b82f0f3b14055d0d598adb7cfb3c2
This commit is contained in:
James E. Blair 2018-08-29 08:43:18 -07:00
parent 4193b61d13
commit 56f90923ed
5 changed files with 128 additions and 18 deletions

View File

@ -2668,10 +2668,11 @@ class ZuulTestCase(BaseTestCase):
fn = os.path.join(key_root, '.version')
with open(fn, 'w') as f:
f.write('1')
# secrets key
private_key_file = os.path.join(
key_root, 'secrets', 'project', source, project, '0.pem')
private_key_dir = os.path.dirname(private_key_file)
self.log.debug("Installing test keys for project %s at %s" % (
self.log.debug("Installing test secrets keys for project %s at %s" % (
project, private_key_file))
if not os.path.isdir(private_key_dir):
os.makedirs(private_key_dir)
@ -2679,6 +2680,18 @@ class ZuulTestCase(BaseTestCase):
with open(private_key_file, 'w') as o:
o.write(i.read())
# ssh key
private_key_file = os.path.join(
key_root, 'ssh', 'project', source, project, '0.pem')
private_key_dir = os.path.dirname(private_key_file)
self.log.debug("Installing test ssh keys for project %s at %s" % (
project, private_key_file))
if not os.path.isdir(private_key_dir):
os.makedirs(private_key_dir)
with open(os.path.join(FIXTURE_DIR, 'ssh.pem')) as i:
with open(private_key_file, 'w') as o:
o.write(i.read())
def setupZK(self):
self.zk_chroot_fixture = self.useFixture(
ChrootedKazooFixture(self.id()))
@ -2727,8 +2740,11 @@ class ZuulTestCase(BaseTestCase):
if self.create_project_keys:
return
with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
test_key = i.read()
test_keys = []
key_fns = ['private.pem', 'ssh.pem']
for fn in key_fns:
with open(os.path.join(FIXTURE_DIR, fn)) as i:
test_keys.append(i.read())
key_root = os.path.join(self.state_root, 'keys')
for root, dirname, files in os.walk(key_root):
@ -2736,7 +2752,7 @@ class ZuulTestCase(BaseTestCase):
if fn == '.version':
continue
with open(os.path.join(root, fn)) as f:
self.assertEqual(test_key, f.read())
self.assertTrue(f.read() in test_keys)
def assertFinalState(self):
self.log.debug("Assert final state")

27
tests/fixtures/ssh.pem vendored Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA2coHon7sFMbXqeRDUzydrFvp5qpHIbZBfaN8sJyaSiYFYkUz
kMW1Kcn1mRVUps2kBo2qzvCjDsOsNGGdbPnDxwrT+ssN3I+7stWVrcc6Sh9gFFvt
7O+F8a1FSpO/L3CkOBhYd/r0TLZ/UK6fBY/7E9lmnhoXpZxpvBzoxGHYB/0ohZG0
6o1cWBcmC6wvvaoYeX7udUuWm6Wlx32DN++IErz1iwrayNflrOyd41fgn35RD3sd
ay9ezlQJrautnyjgJfPXWMbzXgPN0tcpRBCiFmVRRcI2qVPHeXWRS3yhNHPYpOTX
CA4Woc0iY08HAicg8yjIGnT8GisRAfvpqRZM0wIDAQABAoIBAQCanpRNCU8ScRkr
xKMHtUE73QVyffGCPaLBUBB2Urg3bEbmPbseTT8RLBDxXfN7eQO6o1lhEfaxxLm9
dpANjkUwSr+0jfSJYoIftQNPHOKFPUE5Mwr37BVsP1eyWrKhO5dbO+2TQNewnuBE
p7S+fjoDHZV9KYkgSqvGob+frNdy0zjF7LbRLKbnGiVudMq0zNZ/E77XwKXDW4+U
2P6JTR+0jing7gRSFmCgVePBuo1aJO+F+Tr8wHqvArcYgDjn5jFW7xCQR53onKFS
FZVMTVERAAu1dqE5Ucsamy/N67Yu7jGRB/Vwa5WYbvjl23UjbOJiRt/EG+sf4doJ
/FywJ4gBAoGBAPs5o1ZAWFZEXsRbzR+ao4Vou6CaBdioR/h7xhS3xs4GAewmQfKK
cl8lqSd4a6rIwrnEwcvMOnJ0mP+if7ZoRrkK0RYR5A1qoEShTGz9xDyM5deg8nqK
VhvwkLZg20O1wtkr7mXun0pPs6s6lcjtuBZ4hPX8dTphfHLw5MsF8HiDAoGBAN3t
tKNXnPI/uyEzEoMOHs826bKt2aawGfagAUXRFdaQLPXEbuiYZT8YvwnUv2gUbmu+
WeLBI3Oo+YJSs8r6JUVnuOXm+S45fj5I1Su2ykxecZWFG1GDa4LLlp/iYUEtgDmU
HMng6PRxD9zPha7EqirKsvOCYWO5qscGZzFoUYlxAoGAYJ6BQCnND5iJ7fD0ieQa
YbOu/YxfFT1bOKi5vLwVXKUY1i68jEBMzmUYklKQ7gT6RyHx+qRYEi7frOldPtUJ
5h7P3TISSEqqytpSH1TVxQfXWb/PoetURLiXn1zO11KvVoC71j4YyyauDfuhIb6z
XwkI8eYfW82kZDxbce2d12sCgYEAvy8qMJUnhaHViZI/3lrpu8Uoql8OY4TNuSK6
NfUbhQ4LTWX9za6LekHNQaDfi8AeJ/+B29BaxCbLW7P3Y2L/fL0QEi5ad7Hbybhg
vBnqSMQLwa07jYtTsQfGKNKSyd1y2yd3bYqt5PcJnUXBen+9wMOCSjkFwS2Pq4ke
mPevVmECgYEAu5O2qgo25gEorjpu1q6qRcPQ3hSi5i6yKK9JywTxCCZdRjfyqPFs
M/2T6BQLxfffdcgIrstvnK2tEqugA292bKp2WAs6nXx6/qMM9CO+dq6zlYenLPwo
m6pb4KKm38dLDoMvtn2cqkpcR5Mr8sAEAEHNBD3quLSJZhFLXpo3X58=
-----END RSA PRIVATE KEY-----

View File

@ -21,6 +21,8 @@ import gc
import time
from unittest import skip
import paramiko
import zuul.configloader
from zuul.lib import encryption
from tests.base import (
@ -2685,24 +2687,40 @@ class TestProjectKeys(ZuulTestCase):
tenant_config_file = 'config/in-repo/main.yaml'
def test_key_generation(self):
test_keys = []
key_fns = ['private.pem', 'ssh.pem']
for fn in key_fns:
with open(os.path.join(FIXTURE_DIR, fn)) as i:
test_keys.append(i.read())
key_root = os.path.join(self.state_root, 'keys')
private_key_file = os.path.join(
secrets_key_file = os.path.join(
key_root,
'secrets/project/gerrit/org/project/0.pem')
# Make sure that a proper key was created on startup
with open(private_key_file, "rb") as f:
private_key, public_key = \
with open(secrets_key_file, "rb") as f:
private_secrets_key, public_secrets_key = \
encryption.deserialize_rsa_keypair(f.read())
with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
fixture_private_key = i.read()
# Make sure that we didn't just end up with the static fixture
# key
self.assertNotEqual(fixture_private_key, private_key)
self.assertTrue(private_secrets_key not in test_keys)
# Make sure it's the right length
self.assertEqual(4096, private_key.key_size)
self.assertEqual(4096, private_secrets_key.key_size)
ssh_key_file = os.path.join(
key_root,
'ssh/project/gerrit/org/project/0.pem')
# Make sure that a proper key was created on startup
ssh_key = paramiko.RSAKey.from_private_key_file(ssh_key_file)
# Make sure that we didn't just end up with the static fixture
# key
self.assertTrue(private_secrets_key not in test_keys)
# Make sure it's the right length
self.assertEqual(2048, ssh_key.get_bits())
class RoleTestCase(ZuulTestCase):

View File

@ -1348,15 +1348,22 @@ class TenantParser(object):
project.private_secrets_key_file = \
self.keystorage.getProjectSecretsKeyFile(
connection_name, project.name)
project.private_ssh_key_file = \
self.keystorage.getProjectSSHKeyFile(
connection_name, project.name)
self._generateKeys(project)
self._loadKeys(project)
(project.private_ssh_key, project.public_ssh_key) = \
self.keystorage.getProjectSSHKeys(connection_name, project.name)
def _generateKeys(self, project):
if os.path.isfile(project.private_secrets_key_file):
filename = project.private_secrets_key_file
if os.path.isfile(filename):
return
key_dir = os.path.dirname(project.private_secrets_key_file)
key_dir = os.path.dirname(filename)
if not os.path.isdir(key_dir):
os.makedirs(key_dir, 0o700)
@ -1370,16 +1377,15 @@ class TenantParser(object):
# because the public key can be constructed from it.
self.log.info(
"Saving RSA keypair for project %s to %s" % (
project.name, project.private_secrets_key_file)
project.name, filename)
)
# Ensure private key is read/write for zuul user only.
with open(os.open(project.private_secrets_key_file,
with open(os.open(filename,
os.O_CREAT | os.O_WRONLY, 0o600), 'wb') as f:
f.write(pem_private_key)
@staticmethod
def _loadKeys(project):
def _loadKeys(self, project):
# Check the key files specified are there
if not os.path.isfile(project.private_secrets_key_file):
raise Exception(

View File

@ -16,6 +16,10 @@ import tempfile
import logging
import os
import paramiko
RSA_KEY_SIZE = 2048
class Migration(object):
log = logging.getLogger("zuul.KeyStorage")
@ -119,6 +123,7 @@ class MigrationV1(Migration):
class KeyStorage(object):
log = logging.getLogger("zuul.KeyStorage")
current_version = MigrationV1
def __init__(self, root):
@ -133,3 +138,41 @@ class KeyStorage(object):
version = '0'
return os.path.join(self.root, 'secrets', 'project',
connection, project, version + '.pem')
def getProjectSSHKeyFile(self, connection, project, version=None):
"""Return the path to the private ssh key for the project"""
# We don't actually support multiple versions yet
if version is None:
version = '0'
return os.path.join(self.root, 'ssh', 'project',
connection, project, version + '.pem')
def getProjectSSHKeys(self, connection, project):
"""Return the private and public SSH keys for the project
A new key will be created if necessary.
:returns: A tuple containing the PEM encoded private key and
base64 encoded public key.
"""
private_key_file = self.getProjectSSHKeyFile(connection, project)
if not os.path.exists(private_key_file):
self.log.info(
"Generating SSH public key for project %s", project
)
self._createSSHKey(private_key_file)
key = paramiko.RSAKey.from_private_key_file(private_key_file)
with open(private_key_file, 'r') as f:
private_key = f.read()
public_key = key.get_base64()
return (private_key, public_key)
def _createSSHKey(self, fn):
key_dir = os.path.dirname(fn)
if not os.path.isdir(key_dir):
os.makedirs(key_dir, 0o700)
pk = paramiko.RSAKey.generate(bits=RSA_KEY_SIZE)
pk.write_private_key_file(fn)