Per-project vpns, certificates, and revocation

This commit is contained in:
Vishvananda Ishaya 2010-11-06 00:02:36 +00:00
parent 671b712a5a
commit f127d85d77
17 changed files with 529 additions and 291 deletions

View File

@ -16,16 +16,24 @@
# License for the specific language governing permissions and limitations
# under the License.
# ARG is the id of the user
export SUBJ="/C=US/ST=California/L=MountainView/O=AnsoLabs/OU=NovaDev/CN=customer-intCA-$1"
mkdir INTER/$1
cd INTER/$1
# $1 is the id of the project and $2 is the subject of the cert
NAME=$1
SUBJ=$2
mkdir -p projects/$NAME
cd projects/$NAME
cp ../../openssl.cnf.tmpl openssl.cnf
sed -i -e s/%USERNAME%/$1/g openssl.cnf
sed -i -e s/%USERNAME%/$NAME/g openssl.cnf
mkdir certs crl newcerts private
openssl req -new -x509 -extensions v3_ca -keyout private/cakey.pem -out cacert.pem -days 365 -config ./openssl.cnf -batch -nodes
echo "10" > serial
touch index.txt
openssl genrsa -out private/cakey.pem 1024 -config ./openssl.cnf -batch -nodes
openssl req -new -sha2 -key private/cakey.pem -out ../../reqs/inter$1.csr -batch -subj "$SUBJ"
cd ../../
openssl ca -extensions v3_ca -days 365 -out INTER/$1/cacert.pem -in reqs/inter$1.csr -config openssl.cnf -batch
# NOTE(vish): Disabling intermediate ca's because we don't actually need them.
# It makes more sense to have each project have its own root ca.
# openssl genrsa -out private/cakey.pem 1024 -config ./openssl.cnf -batch -nodes
# openssl req -new -sha256 -key private/cakey.pem -out ../../reqs/inter$NAME.csr -batch -subj "$SUBJ"
openssl ca -gencrl -config ./openssl.cnf -out crl.pem
if [ "`id -u`" != "`grep nova /etc/passwd | cut -d':' -f3`" ]; then
sudo chown -R nova:nogroup .
fi
# cd ../../
# openssl ca -extensions v3_ca -days 365 -out INTER/$NAME/cacert.pem -in reqs/inter$NAME.csr -config openssl.cnf -batch

View File

@ -25,4 +25,5 @@ else
openssl req -new -x509 -extensions v3_ca -keyout private/cakey.pem -out cacert.pem -days 365 -config ./openssl.cnf -batch -nodes
touch index.txt
echo "10" > serial
openssl ca -gencrl -config ./openssl.cnf -out crl.pem
fi

View File

@ -24,7 +24,6 @@ dir = .
[ ca ]
default_ca = CA_default
unique_subject = no
[ CA_default ]
serial = $dir/serial
@ -32,6 +31,8 @@ database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
private_key = $dir/private/cakey.pem
unique_subject = no
default_crl_days = 365
default_days = 365
default_md = md5
preserve = no

View File

@ -69,6 +69,7 @@ if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
sys.path.insert(0, possible_topdir)
from nova import context
from nova import crypto
from nova import db
from nova import exception
from nova import flags
@ -93,32 +94,36 @@ class VpnCommands(object):
self.manager = manager.AuthManager()
self.pipe = pipelib.CloudPipe()
def list(self):
"""Print a listing of the VPNs for all projects."""
def list(self, project=None):
"""Print a listing of the VPN data for one or all projects.
args: [project=all]"""
print "%-12s\t" % 'project',
print "%-20s\t" % 'ip:port',
print "%-20s\t" % 'private_ip',
print "%s" % 'state'
for project in self.manager.get_projects():
if project:
projects = [self.manager.get_project(project)]
else:
projects = self.manager.get_projects()
for project in projects:
print "%-12s\t" % project.name,
try:
s = "%s:%s" % (project.vpn_ip, project.vpn_port)
except exception.NotFound:
s = "None"
print "%-20s\t" % s,
ipport = "%s:%s" % (project.vpn_ip, project.vpn_port)
print "%-20s\t" % ipport,
vpn = self._vpn_for(project.id)
if vpn:
command = "ping -c1 -w1 %s > /dev/null; echo $?"
out, _err = utils.execute(command % vpn['private_dns_name'],
check_exit_code=False)
if out.strip() == '0':
net = 'up'
else:
net = 'down'
print vpn['private_dns_name'],
print vpn['node_name'],
print vpn['instance_id'],
net = 'down'
address = None
if vpn.get('fixed_ip', None):
address = vpn['fixed_ip']['address']
command = "ping -c1 -w1 %s > /dev/null; echo $?"
out, _err = utils.execute(command % address,
check_exit_code=False)
if out.strip() == '0':
net = 'up'
print address,
print vpn['host'],
print vpn['ec2_id'],
print vpn['state_description'],
print net
@ -127,11 +132,11 @@ class VpnCommands(object):
def _vpn_for(self, project_id):
"""Get the VPN instance for a project ID."""
for instance in db.instance_get_all(context.get_admin_context()):
ctxt = context.get_admin_context()
for instance in db.instance_get_all_by_project(ctxt, project_id):
if (instance['image_id'] == FLAGS.vpn_image_id
and not instance['state_description'] in
['shutting_down', 'shutdown']
and instance['project_id'] == project_id):
['shutting_down', 'shutdown']):
return instance
def spawn(self):
@ -146,6 +151,22 @@ class VpnCommands(object):
"""Start the VPN for a given project."""
self.pipe.launch_vpn_instance(project_id)
def change(self, project_id, ip, port):
"""Change the ip and port for a vpn.
args: project, ip, port"""
project = self.manager.get_project(project_id)
if not project:
print 'No project %s' % (project_id)
return
admin = context.get_admin_context()
network_ref = db.project_get_network(admin, project_id)
db.network_update(admin,
network_ref['id'],
{'vpn_public_address': ip,
'vpn_public_port': int(port)})
class ShellCommands(object):
def bpython(self):
@ -292,6 +313,14 @@ class UserCommands(object):
is_admin = False
self.manager.modify_user(name, access_key, secret_key, is_admin)
def revoke(self, user_id, project_id=None):
"""revoke certs for a user
arguments: user_id [project_id]"""
if project_id:
crypto.revoke_certs_by_user_and_project(user_id, project_id)
else:
crypto.revoke_certs_by_user(user_id)
class ProjectCommands(object):
"""Class for managing projects."""

View File

@ -25,7 +25,6 @@ import webob.dec
from nova import flags
from nova import wsgi
from nova.api import cloudpipe
from nova.api import ec2
from nova.api import openstack
from nova.api.ec2 import metadatarequesthandler
@ -74,7 +73,6 @@ class API(wsgi.Router):
mapper.connect('%s/{path_info:.*}' % s, controller=mrh,
conditions=ec2api_subdomain)
mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API())
super(API, self).__init__(mapper)
@webob.dec.wsgify

View File

@ -1,69 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
REST API Request Handlers for CloudPipe
"""
import logging
import urllib
import webob
import webob.dec
import webob.exc
from nova import crypto
from nova import wsgi
from nova.auth import manager
from nova.api.ec2 import cloud
_log = logging.getLogger("api")
_log.setLevel(logging.DEBUG)
class API(wsgi.Application):
def __init__(self):
self.controller = cloud.CloudController()
@webob.dec.wsgify
def __call__(self, req):
if req.method == 'POST':
return self.sign_csr(req)
_log.debug("Cloudpipe path is %s" % req.path_info)
if req.path_info.endswith("/getca/"):
return self.send_root_ca(req)
return webob.exc.HTTPNotFound()
def get_project_id_from_ip(self, ip):
# TODO(eday): This was removed with the ORM branch, fix!
instance = self.controller.get_instance_by_ip(ip)
return instance['project_id']
def send_root_ca(self, req):
_log.debug("Getting root ca")
project_id = self.get_project_id_from_ip(req.remote_addr)
res = webob.Response()
res.headers["Content-Type"] = "text/plain"
res.body = crypto.fetch_ca(project_id)
return res
def sign_csr(self, req):
project_id = self.get_project_id_from_ip(req.remote_addr)
cert = self.str_params['cert']
return crypto.sign_csr(urllib.unquote(cert), project_id)

View File

@ -64,12 +64,8 @@ flags.DEFINE_string('credential_key_file', 'pk.pem',
'Filename of private key in credentials zip')
flags.DEFINE_string('credential_cert_file', 'cert.pem',
'Filename of certificate in credentials zip')
flags.DEFINE_string('credential_rc_file', 'novarc',
flags.DEFINE_string('credential_rc_file', '%src',
'Filename of rc in credentials zip')
flags.DEFINE_string('credential_cert_subject',
'/C=US/ST=California/L=MountainView/O=AnsoLabs/'
'OU=NovaDev/CN=%s-%s',
'Subject for certificate for users')
flags.DEFINE_string('auth_driver', 'nova.auth.dbdriver.DbDriver',
'Driver that auth manager uses')
@ -625,27 +621,37 @@ class AuthManager(object):
with self.driver() as drv:
drv.modify_user(uid, access_key, secret_key, admin)
def get_credentials(self, user, project=None):
def get_credentials(self, user, project=None, use_dmz=True):
"""Get credential zip for user in project"""
if not isinstance(user, User):
user = self.get_user(user)
if project is None:
project = user.id
pid = Project.safe_id(project)
rc = self.__generate_rc(user.access, user.secret, pid)
private_key, signed_cert = self._generate_x509_cert(user.id, pid)
private_key, signed_cert = crypto.generate_x509_cert(user.id, pid)
tmpdir = tempfile.mkdtemp()
zf = os.path.join(tmpdir, "temp.zip")
zippy = zipfile.ZipFile(zf, 'w')
zippy.writestr(FLAGS.credential_rc_file, rc)
if use_dmz and FLAGS.region_list:
regions = {}
for item in FLAGS.region_list:
region, _sep, region_host = item.partition("=")
regions[region] = region_host
else:
regions = {'nova': FLAGS.cc_host}
for region, host in regions.iteritems():
rc = self.__generate_rc(user.access,
user.secret,
pid,
use_dmz,
host)
zippy.writestr(FLAGS.credential_rc_file % region, rc)
zippy.writestr(FLAGS.credential_key_file, private_key)
zippy.writestr(FLAGS.credential_cert_file, signed_cert)
try:
(vpn_ip, vpn_port) = self.get_project_vpn_data(project)
except exception.NotFound:
vpn_ip = None
(vpn_ip, vpn_port) = self.get_project_vpn_data(project)
if vpn_ip:
configfile = open(FLAGS.vpn_client_template, "r")
s = string.Template(configfile.read())
@ -656,10 +662,9 @@ class AuthManager(object):
port=vpn_port)
zippy.writestr(FLAGS.credential_vpn_file, config)
else:
logging.warn("No vpn data for project %s" %
pid)
LOG.warn("No vpn data for project %s", pid)
zippy.writestr(FLAGS.ca_file, crypto.fetch_ca(user.id))
zippy.writestr(FLAGS.ca_file, crypto.fetch_ca(pid))
zippy.close()
with open(zf, 'rb') as f:
read_buffer = f.read()
@ -667,38 +672,38 @@ class AuthManager(object):
shutil.rmtree(tmpdir)
return read_buffer
def get_environment_rc(self, user, project=None):
def get_environment_rc(self, user, project=None, use_dmz=True):
"""Get credential zip for user in project"""
if not isinstance(user, User):
user = self.get_user(user)
if project is None:
project = user.id
pid = Project.safe_id(project)
return self.__generate_rc(user.access, user.secret, pid)
return self.__generate_rc(user.access, user.secret, pid, use_dmz)
@staticmethod
def __generate_rc(access, secret, pid):
def __generate_rc(access, secret, pid, use_dmz=True, host=None):
"""Generate rc file for user"""
if use_dmz:
cc_host = FLAGS.cc_dmz
else:
cc_host = FLAGS.cc_host
# NOTE(vish): Always use the dmz since it is used from inside the
# instance
s3_host = FLAGS.s3_dmz
if host:
s3_host = host
cc_host = host
rc = open(FLAGS.credentials_template).read()
rc = rc % {'access': access,
'project': pid,
'secret': secret,
'ec2': FLAGS.ec2_url,
's3': 'http://%s:%s' % (FLAGS.s3_host, FLAGS.s3_port),
'ec2': '%s://%s:%s%s' % (FLAGS.ec2_prefix,
cc_host,
FLAGS.cc_port,
FLAGS.ec2_suffix),
's3': 'http://%s:%s' % (s3_host, FLAGS.s3_port),
'nova': FLAGS.ca_file,
'cert': FLAGS.credential_cert_file,
'key': FLAGS.credential_key_file}
return rc
def _generate_x509_cert(self, uid, pid):
"""Generate x509 cert for user"""
(private_key, csr) = crypto.generate_x509_cert(
self.__cert_subject(uid))
# TODO(joshua): This should be async call back to the cloud controller
signed_cert = crypto.sign_csr(csr, pid)
return (private_key, signed_cert)
@staticmethod
def __cert_subject(uid):
"""Helper to generate cert subject"""
return FLAGS.credential_cert_subject % (uid, utils.isotime())

View File

@ -1,63 +0,0 @@
#!/bin/bash
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# This gets zipped and run on the cloudpipe-managed OpenVPN server
export SUPERVISOR="http://10.255.255.1:8773/cloudpipe"
export VPN_IP=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{print $1}'`
export BROADCAST=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f3 | awk '{print $1}'`
export DHCP_MASK=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f4 | awk '{print $1}'`
export GATEWAY=`netstat -r | grep default | cut -d' ' -f10`
export SUBJ="/C=US/ST=California/L=MountainView/O=AnsoLabs/OU=NovaDev/CN=customer-vpn-$VPN_IP"
DHCP_LOWER=`echo $BROADCAST | awk -F. '{print $1"."$2"."$3"." $4 - 10 }'`
DHCP_UPPER=`echo $BROADCAST | awk -F. '{print $1"."$2"."$3"." $4 - 1 }'`
# generate a server DH
openssl dhparam -out /etc/openvpn/dh1024.pem 1024
# generate a server priv key
openssl genrsa -out /etc/openvpn/server.key 2048
# generate a server CSR
openssl req -new -key /etc/openvpn/server.key -out /etc/openvpn/server.csr -batch -subj "$SUBJ"
# URLEncode the CSR
CSRTEXT=`cat /etc/openvpn/server.csr`
CSRTEXT=$(python -c "import urllib; print urllib.quote('''$CSRTEXT''')")
# SIGN the csr and save as server.crt
# CURL fetch to the supervisor, POSTing the CSR text, saving the result as the CRT file
curl --fail $SUPERVISOR -d "cert=$CSRTEXT" > /etc/openvpn/server.crt
curl --fail $SUPERVISOR/getca/ > /etc/openvpn/ca.crt
# Customize the server.conf.template
cd /etc/openvpn
sed -e s/VPN_IP/$VPN_IP/g server.conf.template > server.conf
sed -i -e s/DHCP_SUBNET/$DHCP_MASK/g server.conf
sed -i -e s/DHCP_LOWER/$DHCP_LOWER/g server.conf
sed -i -e s/DHCP_UPPER/$DHCP_UPPER/g server.conf
sed -i -e s/max-clients\ 1/max-clients\ 10/g server.conf
echo "\npush \"route 10.255.255.1 255.255.255.255 $GATEWAY\"\n" >> server.conf
echo "\npush \"route 10.255.255.253 255.255.255.255 $GATEWAY\"\n" >> server.conf
echo "\nduplicate-cn\n" >> server.conf
/etc/init.d/openvpn start

View File

@ -0,0 +1,50 @@
#!/bin/bash
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# This gets zipped and run on the cloudpipe-managed OpenVPN server
export VPN_IP=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{print $$1}'`
export BROADCAST=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f3 | awk '{print $$1}'`
export DHCP_MASK=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f4 | awk '{print $$1}'`
export GATEWAY=`netstat -r | grep default | cut -d' ' -f10`
DHCP_LOWER=`echo $$BROADCAST | awk -F. '{print $$1"."$$2"."$$3"." $$4 - ${num_vpn} }'`
DHCP_UPPER=`echo $$BROADCAST | awk -F. '{print $$1"."$$2"."$$3"." $$4 - 1 }'`
# generate a server DH
openssl dhparam -out /etc/openvpn/dh1024.pem 1024
cp crl.pem /etc/openvpn/
cp server.key /etc/openvpn/
cp ca.crt /etc/openvpn/
cp server.crt /etc/openvpn/
# Customize the server.conf.template
cd /etc/openvpn
sed -e s/VPN_IP/$$VPN_IP/g server.conf.template > server.conf
sed -i -e s/DHCP_SUBNET/$$DHCP_MASK/g server.conf
sed -i -e s/DHCP_LOWER/$$DHCP_LOWER/g server.conf
sed -i -e s/DHCP_UPPER/$$DHCP_UPPER/g server.conf
sed -i -e s/max-clients\ 1/max-clients\ 10/g server.conf
echo "push \"route ${dmz_net} ${dmz_mask} $$GATEWAY\"" >> server.conf
echo "duplicate-cn" >> server.conf
echo "crl-verify /etc/openvpn/crl.pem" >> server.conf
/etc/init.d/openvpn start

View File

@ -22,13 +22,15 @@ an instance with it.
"""
import base64
import logging
import os
import string
import tempfile
import zipfile
from nova import context
from nova import crypto
from nova import db
from nova import exception
from nova import flags
from nova import utils
@ -39,8 +41,17 @@ from nova.api.ec2 import cloud
FLAGS = flags.FLAGS
flags.DEFINE_string('boot_script_template',
utils.abspath('cloudpipe/bootscript.sh'),
utils.abspath('cloudpipe/bootscript.template'),
'Template for script to run on cloudpipe instance boot')
flags.DEFINE_string('dmz_net',
'10.0.0.0',
'Network to push into openvpn config')
flags.DEFINE_string('dmz_mask',
'255.255.255.0',
'Netmask to push into openvpn config')
LOG = logging.getLogger('nova-cloudpipe')
class CloudPipe(object):
@ -48,64 +59,96 @@ class CloudPipe(object):
self.controller = cloud.CloudController()
self.manager = manager.AuthManager()
def launch_vpn_instance(self, project_id):
logging.debug("Launching VPN for %s" % (project_id))
project = self.manager.get_project(project_id)
def get_encoded_zip(self, project_id):
# Make a payload.zip
tmpfolder = tempfile.mkdtemp()
filename = "payload.zip"
zippath = os.path.join(tmpfolder, filename)
z = zipfile.ZipFile(zippath, "w", zipfile.ZIP_DEFLATED)
z.write(FLAGS.boot_script_template, 'autorun.sh')
shellfile = open(FLAGS.boot_script_template, "r")
s = string.Template(shellfile.read())
shellfile.close()
boot_script = s.substitute(cc_dmz=FLAGS.cc_dmz,
cc_port=FLAGS.cc_port,
dmz_net=FLAGS.dmz_net,
dmz_mask=FLAGS.dmz_mask,
num_vpn=FLAGS.cnt_vpn_clients)
# genvpn, sign csr
crypto.generate_vpn_files(project_id)
z.writestr('autorun.sh', boot_script)
crl = os.path.join(crypto.ca_folder(project_id), 'crl.pem')
z.write(crl, 'crl.pem')
server_key = os.path.join(crypto.ca_folder(project_id), 'server.key')
z.write(server_key, 'server.key')
ca_crt = os.path.join(crypto.ca_path(project_id))
z.write(ca_crt, 'ca.crt')
server_crt = os.path.join(crypto.ca_folder(project_id), 'server.crt')
z.write(server_crt, 'server.crt')
z.close()
key_name = self.setup_key_pair(project.project_manager_id, project_id)
zippy = open(zippath, "r")
context = context.RequestContext(user=project.project_manager,
project=project)
# NOTE(vish): run instances expects encoded userdata, it is decoded
# in the get_metadata_call. autorun.sh also decodes the zip file,
# hence the double encoding.
encoded = zippy.read().encode("base64").encode("base64")
zippy.close()
return encoded
reservation = self.controller.run_instances(context,
# Run instances expects encoded userdata, it is decoded in the
# get_metadata_call. autorun.sh also decodes the zip file, hence
# the double encoding.
user_data=zippy.read().encode("base64").encode("base64"),
def launch_vpn_instance(self, project_id):
LOG.debug("Launching VPN for %s" % (project_id))
project = self.manager.get_project(project_id)
ctxt = context.RequestContext(user=project.project_manager,
project=project)
key_name = self.setup_key_pair(ctxt)
group_name = self.setup_security_group(ctxt)
reservation = self.controller.run_instances(ctxt,
user_data=self.get_encoded_zip(project_id),
max_count=1,
min_count=1,
instance_type='m1.tiny',
image_id=FLAGS.vpn_image_id,
key_name=key_name,
security_groups=["vpn-secgroup"])
zippy.close()
security_group=[group_name])
def setup_key_pair(self, user_id, project_id):
key_name = '%s%s' % (project_id, FLAGS.vpn_key_suffix)
def setup_security_group(self, context):
group_name = '%s%s' % (context.project.id, FLAGS.vpn_key_suffix)
if db.security_group_exists(context, context.project.id, group_name):
return group_name
group = {'user_id': context.user.id,
'project_id': context.project.id,
'name': group_name,
'description': 'Group for vpn'}
group_ref = db.security_group_create(context, group)
rule = {'parent_group_id': group_ref['id'],
'cidr': '0.0.0.0/0',
'protocol': 'udp',
'from_port': 1194,
'to_port': 1194}
db.security_group_rule_create(context, rule)
rule = {'parent_group_id': group_ref['id'],
'cidr': '0.0.0.0/0',
'protocol': 'icmp',
'from_port': -1,
'to_port': -1}
db.security_group_rule_create(context, rule)
# NOTE(vish): No need to trigger the group since the instance
# has not been run yet.
return group_name
def setup_key_pair(self, context):
key_name = '%s%s' % (context.project.id, FLAGS.vpn_key_suffix)
try:
private_key, fingerprint = self.manager.generate_key_pair(user_id,
key_name)
result = cloud._gen_key(context, context.user.id, key_name)
private_key = result['private_key']
try:
key_dir = os.path.join(FLAGS.keys_path, user_id)
key_dir = os.path.join(FLAGS.keys_path, context.user.id)
if not os.path.exists(key_dir):
os.makedirs(key_dir)
file_name = os.path.join(key_dir, '%s.pem' % key_name)
with open(file_name, 'w') as f:
key_path = os.path.join(key_dir, '%s.pem' % key_name)
with open(key_path, 'w') as f:
f.write(private_key)
except:
pass
except exception.Duplicate:
pass
return key_name
# def setup_secgroups(self, username):
# conn = self.euca.connection_for(username)
# try:
# secgroup = conn.create_security_group("vpn-secgroup",
# "vpn-secgroup")
# secgroup.authorize(ip_protocol = "udp", from_port = "1194",
# to_port = "1194", cidr_ip = "0.0.0.0/0")
# secgroup.authorize(ip_protocol = "tcp", from_port = "80",
# to_port = "80", cidr_ip = "0.0.0.0/0")
# secgroup.authorize(ip_protocol = "tcp", from_port = "22",
# to_port = "22", cidr_ip = "0.0.0.0/0")
# except:
# pass

View File

@ -17,7 +17,7 @@
# under the License.
"""
Wrappers around standard crypto, including root and intermediate CAs,
Wrappers around standard crypto, including root and project CAs,
SSH key_pairs and x509 certificates.
"""
@ -33,28 +33,57 @@ import utils
import M2Crypto
from nova import exception
from nova import context
from nova import db
from nova import flags
FLAGS = flags.FLAGS
flags.DEFINE_string('ca_file', 'cacert.pem', 'Filename of root CA')
flags.DEFINE_string('key_file',
os.path.join('private', 'cakey.pem'),
'Filename of private key')
flags.DEFINE_string('crl_file', 'crl.pem',
'Filename of root Certificate Revokation List')
flags.DEFINE_string('keys_path', utils.abspath('../keys'),
'Where we keep our keys')
flags.DEFINE_string('ca_path', utils.abspath('../CA'),
'Where we keep our root CA')
flags.DEFINE_boolean('use_intermediate_ca', False,
'Should we use intermediate CAs for each project?')
flags.DEFINE_boolean('use_project_ca', False,
'Should we use a CA for each project?')
flags.DEFINE_string('user_cert_subject',
'/C=US/ST=California/L=MountainView/O=AnsoLabs/'
'OU=NovaDev/CN=%s-%s-%s',
'Subject for certificate for users, '
'%s for project, user, timestamp')
flags.DEFINE_string('project_cert_subject',
'/C=US/ST=California/L=MountainView/O=AnsoLabs/'
'OU=NovaDev/CN=project-ca-%s-%s',
'Subject for certificate for projects, '
'%s for project, timestamp')
flags.DEFINE_string('vpn_cert_subject',
'/C=US/ST=California/L=MountainView/O=AnsoLabs/'
'OU=NovaDev/CN=project-vpn-%s-%s',
'Subject for certificate for vpns, '
'%s for project, timestamp')
def ca_path(project_id):
if project_id:
return "%s/INTER/%s/cacert.pem" % (FLAGS.ca_path, project_id)
return "%s/cacert.pem" % (FLAGS.ca_path)
def ca_folder(project_id=None):
if FLAGS.use_project_ca and project_id:
return os.path.join(FLAGS.ca_path, 'projects', project_id)
return FLAGS.ca_path
def ca_path(project_id=None):
return os.path.join(ca_folder(project_id), FLAGS.ca_file)
def key_path(project_id=None):
return os.path.join(ca_folder(project_id), FLAGS.key_file)
def fetch_ca(project_id=None, chain=True):
if not FLAGS.use_intermediate_ca:
if not FLAGS.use_project_ca:
project_id = None
buffer = ""
if project_id:
@ -91,8 +120,8 @@ def generate_key_pair(bits=1024):
def ssl_pub_to_ssh_pub(ssl_public_key, name='root', suffix='nova'):
pub_key_buffer = M2Crypto.BIO.MemoryBuffer(ssl_public_key)
rsa_key = M2Crypto.RSA.load_pub_key_bio(pub_key_buffer)
buf = M2Crypto.BIO.MemoryBuffer(ssl_public_key)
rsa_key = M2Crypto.RSA.load_pub_key_bio(buf)
e, n = rsa_key.pub()
key_type = 'ssh-rsa'
@ -105,53 +134,137 @@ def ssl_pub_to_ssh_pub(ssl_public_key, name='root', suffix='nova'):
return '%s %s %s@%s\n' % (key_type, b64_blob, name, suffix)
def generate_x509_cert(subject, bits=1024):
def revoke_cert(project_id, file_name):
"""Revoke a cert by file name"""
start = os.getcwd()
os.chdir(ca_folder(project_id))
# NOTE(vish): potential race condition here
utils.execute("openssl ca -config ./openssl.cnf -revoke '%s'" % file_name)
utils.execute("openssl ca -gencrl -config ./openssl.cnf -out '%s'" %
FLAGS.crl_file)
os.chdir(start)
def revoke_certs_by_user(user_id):
"""Revoke all user certs"""
admin = context.get_admin_context()
for cert in db.certificate_get_all_by_user(admin, user_id):
revoke_cert(cert['project_id'], cert['file_name'])
def revoke_certs_by_project(project_id):
"""Revoke all project certs"""
# NOTE(vish): This is somewhat useless because we can just shut down
# the vpn.
admin = context.get_admin_context()
for cert in db.certificate_get_all_by_project(admin, project_id):
revoke_cert(cert['project_id'], cert['file_name'])
def revoke_certs_by_user_and_project(user_id, project_id):
"""Revoke certs for user in project"""
admin = context.get_admin_context()
for cert in db.certificate_get_all_by_user(admin, user_id, project_id):
revoke_cert(cert['project_id'], cert['file_name'])
def _project_cert_subject(project_id):
"""Helper to generate user cert subject"""
return FLAGS.project_cert_subject % (project_id, utils.isotime())
def _vpn_cert_subject(project_id):
"""Helper to generate user cert subject"""
return FLAGS.vpn_cert_subject % (project_id, utils.isotime())
def _user_cert_subject(user_id, project_id):
"""Helper to generate user cert subject"""
return FLAGS.user_cert_subject % (project_id, user_id, utils.isotime())
def generate_x509_cert(user_id, project_id, bits=1024):
"""Generate and sign a cert for user in project"""
subject = _user_cert_subject(user_id, project_id)
tmpdir = tempfile.mkdtemp()
keyfile = os.path.abspath(os.path.join(tmpdir, 'temp.key'))
csrfile = os.path.join(tmpdir, 'temp.csr')
logging.debug("openssl genrsa -out %s %s" % (keyfile, bits))
utils.runthis("Generating private key: %s",
"openssl genrsa -out %s %s" % (keyfile, bits))
utils.runthis("Generating CSR: %s",
"openssl req -new -key %s -out %s -batch -subj %s" %
utils.execute("openssl genrsa -out %s %s" % (keyfile, bits))
utils.execute("openssl req -new -key %s -out %s -batch -subj %s" %
(keyfile, csrfile, subject))
private_key = open(keyfile).read()
csr = open(csrfile).read()
shutil.rmtree(tmpdir)
return (private_key, csr)
(serial, signed_csr) = sign_csr(csr, project_id)
strserial = "%X" % serial
if(len(strserial) % 2):
strserial = "0%s" % strserial
fname = os.path.join(ca_folder(project_id), "newcerts/%s.pem" % strserial)
cert = {'user_id': user_id,
'project_id': project_id,
'file_name': fname}
db.certificate_create(context.get_admin_context(), cert)
return (private_key, signed_csr)
def sign_csr(csr_text, intermediate=None):
if not FLAGS.use_intermediate_ca:
intermediate = None
if not intermediate:
return _sign_csr(csr_text, FLAGS.ca_path)
user_ca = "%s/INTER/%s" % (FLAGS.ca_path, intermediate)
if not os.path.exists(user_ca):
def _ensure_project_folder(project_id):
if not os.path.exists(ca_path(project_id)):
start = os.getcwd()
os.chdir(FLAGS.ca_path)
utils.runthis("Generating intermediate CA: %s",
"sh geninter.sh %s" % (intermediate))
os.chdir(ca_folder())
utils.execute("sh geninter.sh %s %s" %
(project_id, _project_cert_subject(project_id)))
os.chdir(start)
return _sign_csr(csr_text, user_ca)
def generate_vpn_files(project_id):
project_folder = ca_folder(project_id)
csr_fn = os.path.join(project_folder, "server.csr")
crt_fn = os.path.join(project_folder, "server.crt")
if os.path.exists(crt_fn):
return
_ensure_project_folder(project_id)
start = os.getcwd()
os.chdir(ca_folder())
# TODO(vish): the shell scripts could all be done in python
utils.execute("sh genvpn.sh %s %s" %
(project_id, _vpn_cert_subject(project_id)))
with open(csr_fn, "r") as csrfile:
csr_text = csrfile.read()
(serial, signed_csr) = sign_csr(csr_text, project_id)
with open(crt_fn, "w") as crtfile:
crtfile.write(signed_csr)
os.chdir(start)
def sign_csr(csr_text, project_id=None):
if not FLAGS.use_project_ca:
project_id = None
if not project_id:
return _sign_csr(csr_text, ca_folder())
_ensure_project_folder(project_id)
project_folder = ca_folder(project_id)
return _sign_csr(csr_text, ca_folder(project_id))
def _sign_csr(csr_text, ca_folder):
tmpfolder = tempfile.mkdtemp()
csrfile = open("%s/inbound.csr" % (tmpfolder), "w")
inbound = os.path.join(tmpfolder, "inbound.csr")
outbound = os.path.join(tmpfolder, "outbound.csr")
csrfile = open(inbound, "w")
csrfile.write(csr_text)
csrfile.close()
logging.debug("Flags path: %s" % ca_folder)
logging.debug("Flags path: %s", ca_folder)
start = os.getcwd()
# Change working dir to CA
os.chdir(ca_folder)
utils.runthis("Signing cert: %s",
"openssl ca -batch -out %s/outbound.crt "
"-config ./openssl.cnf -infiles %s/inbound.csr" %
(tmpfolder, tmpfolder))
utils.execute("openssl ca -batch -out %s -config "
"./openssl.cnf -infiles %s" % (outbound, inbound))
out, _err = utils.execute("openssl x509 -in %s -serial -noout" % outbound)
serial = int(out.rpartition("=")[2])
os.chdir(start)
with open("%s/outbound.crt" % (tmpfolder), "r") as crtfile:
return crtfile.read()
with open(outbound, "r") as crtfile:
return (serial, crtfile.read())
def mkreq(bits, subject="foo", ca=0):
@ -159,8 +272,7 @@ def mkreq(bits, subject="foo", ca=0):
req = M2Crypto.X509.Request()
rsa = M2Crypto.RSA.gen_key(bits, 65537, callback=lambda: None)
pk.assign_rsa(rsa)
# Should not be freed here
rsa = None
rsa = None # should not be freed here
req.set_pubkey(pk)
req.set_subject(subject)
req.sign(pk, 'sha512')
@ -224,7 +336,6 @@ def mkcacert(subject='nova', years=1):
# IN THE SOFTWARE.
# http://code.google.com/p/boto
def compute_md5(fp):
"""
@type fp: file

View File

@ -117,6 +117,45 @@ def service_update(context, service_id, values):
###################
def certificate_create(context, values):
"""Create a certificate from the values dictionary."""
return IMPL.certificate_create(context, values)
def certificate_destroy(context, certificate_id):
"""Destroy the certificate or raise if it does not exist."""
return IMPL.certificate_destroy(context, certificate_id)
def certificate_get_all_by_project(context, project_id):
"""Get all certificates for a project."""
return IMPL.certificate_get_all_by_project(context, project_id)
def certificate_get_all_by_user(context, user_id):
"""Get all certificates for a user."""
return IMPL.certificate_get_all_by_user(context, user_id)
def certificate_get_all_by_user_and_project(context, user_id, project_id):
"""Get all certificates for a user and project."""
return IMPL.certificate_get_all_by_user_and_project(context,
user_id,
project_id)
def certificate_update(context, certificate_id, values):
"""Set the given properties on an certificate and update it.
Raises NotFound if service does not exist.
"""
return IMPL.service_update(context, certificate_id, values)
###################
def floating_ip_allocate_address(context, host, project_id):
"""Allocate free floating ip and return the address.

View File

@ -253,6 +253,84 @@ def service_update(context, service_id, values):
###################
@require_admin_context
def certificate_get(context, certificate_id, session=None):
if not session:
session = get_session()
result = session.query(models.Certificate).\
filter_by(id=certificate_id).\
filter_by(deleted=can_read_deleted(context)).\
first()
if not result:
raise exception.NotFound('No certificate for id %s' % certificate_id)
return result
@require_admin_context
def certificate_create(context, values):
certificate_ref = models.Certificate()
for (key, value) in values.iteritems():
certificate_ref[key] = value
certificate_ref.save()
return certificate_ref
@require_admin_context
def certificate_destroy(context, certificate_id):
session = get_session()
with session.begin():
certificate_ref = certificate_get(context,
certificate_id,
session=session)
certificate_ref.delete(session=session)
@require_admin_context
def certificate_get_all_by_project(context, project_id):
session = get_session()
return session.query(models.Certificate).\
filter_by(project_id=project_id).\
filter_by(deleted=False).\
all()
@require_admin_context
def certificate_get_all_by_user(context, user_id):
session = get_session()
return session.query(models.Certificate).\
filter_by(user_id=user_id).\
filter_by(deleted=False).\
all()
@require_admin_context
def certificate_get_all_by_user_and_project(_context, user_id, project_id):
session = get_session()
return session.query(models.Certificate).\
filter_by(user_id=user_id).\
filter_by(project_id=project_id).\
filter_by(deleted=False).\
all()
@require_admin_context
def certificate_update(context, certificate_id, values):
session = get_session()
with session.begin():
certificate_ref = certificate_get(context,
certificate_id,
session=session)
for (key, value) in values.iteritems():
certificate_ref[key] = value
certificate_ref.save(session=session)
###################
@require_context
def floating_ip_allocate_address(context, host, project_id):
authorize_project_context(context, project_id)

View File

@ -151,6 +151,16 @@ class Service(BASE, NovaBase):
disabled = Column(Boolean, default=False)
class Certificate(BASE, NovaBase):
"""Represents a an x509 certificate"""
__tablename__ = 'certificates'
id = Column(Integer, primary_key=True)
user_id = Column(String(255))
project_id = Column(String(255))
file_name = Column(String(255))
class Instance(BASE, NovaBase):
"""Represents a guest vm"""
__tablename__ = 'instances'
@ -521,7 +531,7 @@ def register_models():
"""Register Models and create metadata"""
from sqlalchemy import create_engine
models = (Service, Instance, Volume, ExportDevice, IscsiTarget, FixedIp,
FloatingIp, Network, SecurityGroup,
FloatingIp, Network, SecurityGroup, Certificate,
SecurityGroupIngressRule, SecurityGroupInstanceAssociation,
AuthToken, User, Project) # , Image, Host
engine = create_engine(FLAGS.sql_connection, echo=False)

View File

@ -208,17 +208,13 @@ class AuthManagerTestCase(object):
# so it probably belongs in crypto_unittest
# but I'm leaving it where I found it.
with user_and_project_generator(self.manager) as (user, project):
# NOTE(todd): Should mention why we must setup controller first
# (somebody please clue me in)
cloud_controller = cloud.CloudController()
cloud_controller.setup()
_key, cert_str = self.manager._generate_x509_cert('test1',
'testproj')
# NOTE(vish): Setup runs genroot.sh if it hasn't been run
cloud.CloudController().setup()
_key, cert_str = crypto.generate_x509_cert(user.id, project.id)
logging.debug(cert_str)
# Need to verify that it's signed by the right intermediate CA
full_chain = crypto.fetch_ca(project_id='testproj', chain=True)
int_cert = crypto.fetch_ca(project_id='testproj', chain=False)
full_chain = crypto.fetch_ca(project_id=project.id, chain=True)
int_cert = crypto.fetch_ca(project_id=project.id, chain=False)
cloud_cert = crypto.fetch_ca()
logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain)
signed_cert = X509.load_cert_string(cert_str)
@ -227,7 +223,8 @@ class AuthManagerTestCase(object):
cloud_cert = X509.load_cert_string(cloud_cert)
self.assertTrue(signed_cert.verify(chain_cert.get_pubkey()))
self.assertTrue(signed_cert.verify(int_cert.get_pubkey()))
if not FLAGS.use_intermediate_ca:
if not FLAGS.use_project_ca:
self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey()))
else:
self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey()))