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:
parent
4193b61d13
commit
56f90923ed
|
@ -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")
|
||||
|
|
|
@ -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-----
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue