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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
# ARG is the id of the user # $1 is the id of the project and $2 is the subject of the cert
export SUBJ="/C=US/ST=California/L=MountainView/O=AnsoLabs/OU=NovaDev/CN=customer-intCA-$1" NAME=$1
mkdir INTER/$1 SUBJ=$2
cd INTER/$1 mkdir -p projects/$NAME
cd projects/$NAME
cp ../../openssl.cnf.tmpl openssl.cnf 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 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 echo "10" > serial
touch index.txt touch index.txt
openssl genrsa -out private/cakey.pem 1024 -config ./openssl.cnf -batch -nodes # NOTE(vish): Disabling intermediate ca's because we don't actually need them.
openssl req -new -sha2 -key private/cakey.pem -out ../../reqs/inter$1.csr -batch -subj "$SUBJ" # It makes more sense to have each project have its own root ca.
cd ../../ # openssl genrsa -out private/cakey.pem 1024 -config ./openssl.cnf -batch -nodes
openssl ca -extensions v3_ca -days 365 -out INTER/$1/cacert.pem -in reqs/inter$1.csr -config openssl.cnf -batch # 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 openssl req -new -x509 -extensions v3_ca -keyout private/cakey.pem -out cacert.pem -days 365 -config ./openssl.cnf -batch -nodes
touch index.txt touch index.txt
echo "10" > serial echo "10" > serial
openssl ca -gencrl -config ./openssl.cnf -out crl.pem
fi fi

View File

@ -24,7 +24,6 @@ dir = .
[ ca ] [ ca ]
default_ca = CA_default default_ca = CA_default
unique_subject = no
[ CA_default ] [ CA_default ]
serial = $dir/serial serial = $dir/serial
@ -32,6 +31,8 @@ database = $dir/index.txt
new_certs_dir = $dir/newcerts new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem certificate = $dir/cacert.pem
private_key = $dir/private/cakey.pem private_key = $dir/private/cakey.pem
unique_subject = no
default_crl_days = 365
default_days = 365 default_days = 365
default_md = md5 default_md = md5
preserve = no 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) sys.path.insert(0, possible_topdir)
from nova import context from nova import context
from nova import crypto
from nova import db from nova import db
from nova import exception from nova import exception
from nova import flags from nova import flags
@ -93,32 +94,36 @@ class VpnCommands(object):
self.manager = manager.AuthManager() self.manager = manager.AuthManager()
self.pipe = pipelib.CloudPipe() self.pipe = pipelib.CloudPipe()
def list(self): def list(self, project=None):
"""Print a listing of the VPNs for all projects.""" """Print a listing of the VPN data for one or all projects.
args: [project=all]"""
print "%-12s\t" % 'project', print "%-12s\t" % 'project',
print "%-20s\t" % 'ip:port', print "%-20s\t" % 'ip:port',
print "%-20s\t" % 'private_ip',
print "%s" % 'state' 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, print "%-12s\t" % project.name,
ipport = "%s:%s" % (project.vpn_ip, project.vpn_port)
try: print "%-20s\t" % ipport,
s = "%s:%s" % (project.vpn_ip, project.vpn_port)
except exception.NotFound:
s = "None"
print "%-20s\t" % s,
vpn = self._vpn_for(project.id) vpn = self._vpn_for(project.id)
if vpn: if vpn:
command = "ping -c1 -w1 %s > /dev/null; echo $?" net = 'down'
out, _err = utils.execute(command % vpn['private_dns_name'], address = None
check_exit_code=False) if vpn.get('fixed_ip', None):
if out.strip() == '0': address = vpn['fixed_ip']['address']
net = 'up' command = "ping -c1 -w1 %s > /dev/null; echo $?"
else: out, _err = utils.execute(command % address,
net = 'down' check_exit_code=False)
print vpn['private_dns_name'], if out.strip() == '0':
print vpn['node_name'], net = 'up'
print vpn['instance_id'], print address,
print vpn['host'],
print vpn['ec2_id'],
print vpn['state_description'], print vpn['state_description'],
print net print net
@ -127,11 +132,11 @@ class VpnCommands(object):
def _vpn_for(self, project_id): def _vpn_for(self, project_id):
"""Get the VPN instance for a 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 if (instance['image_id'] == FLAGS.vpn_image_id
and not instance['state_description'] in and not instance['state_description'] in
['shutting_down', 'shutdown'] ['shutting_down', 'shutdown']):
and instance['project_id'] == project_id):
return instance return instance
def spawn(self): def spawn(self):
@ -146,6 +151,22 @@ class VpnCommands(object):
"""Start the VPN for a given project.""" """Start the VPN for a given project."""
self.pipe.launch_vpn_instance(project_id) 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): class ShellCommands(object):
def bpython(self): def bpython(self):
@ -292,6 +313,14 @@ class UserCommands(object):
is_admin = False is_admin = False
self.manager.modify_user(name, access_key, secret_key, is_admin) 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 ProjectCommands(object):
"""Class for managing projects.""" """Class for managing projects."""

View File

@ -25,7 +25,6 @@ import webob.dec
from nova import flags from nova import flags
from nova import wsgi from nova import wsgi
from nova.api import cloudpipe
from nova.api import ec2 from nova.api import ec2
from nova.api import openstack from nova.api import openstack
from nova.api.ec2 import metadatarequesthandler from nova.api.ec2 import metadatarequesthandler
@ -74,7 +73,6 @@ class API(wsgi.Router):
mapper.connect('%s/{path_info:.*}' % s, controller=mrh, mapper.connect('%s/{path_info:.*}' % s, controller=mrh,
conditions=ec2api_subdomain) conditions=ec2api_subdomain)
mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API())
super(API, self).__init__(mapper) super(API, self).__init__(mapper)
@webob.dec.wsgify @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') 'Filename of private key in credentials zip')
flags.DEFINE_string('credential_cert_file', 'cert.pem', flags.DEFINE_string('credential_cert_file', 'cert.pem',
'Filename of certificate in credentials zip') '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') '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', flags.DEFINE_string('auth_driver', 'nova.auth.dbdriver.DbDriver',
'Driver that auth manager uses') 'Driver that auth manager uses')
@ -625,27 +621,37 @@ class AuthManager(object):
with self.driver() as drv: with self.driver() as drv:
drv.modify_user(uid, access_key, secret_key, admin) 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""" """Get credential zip for user in project"""
if not isinstance(user, User): if not isinstance(user, User):
user = self.get_user(user) user = self.get_user(user)
if project is None: if project is None:
project = user.id project = user.id
pid = Project.safe_id(project) pid = Project.safe_id(project)
rc = self.__generate_rc(user.access, user.secret, pid) private_key, signed_cert = crypto.generate_x509_cert(user.id, pid)
private_key, signed_cert = self._generate_x509_cert(user.id, pid)
tmpdir = tempfile.mkdtemp() tmpdir = tempfile.mkdtemp()
zf = os.path.join(tmpdir, "temp.zip") zf = os.path.join(tmpdir, "temp.zip")
zippy = zipfile.ZipFile(zf, 'w') 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_key_file, private_key)
zippy.writestr(FLAGS.credential_cert_file, signed_cert) zippy.writestr(FLAGS.credential_cert_file, signed_cert)
try: (vpn_ip, vpn_port) = self.get_project_vpn_data(project)
(vpn_ip, vpn_port) = self.get_project_vpn_data(project)
except exception.NotFound:
vpn_ip = None
if vpn_ip: if vpn_ip:
configfile = open(FLAGS.vpn_client_template, "r") configfile = open(FLAGS.vpn_client_template, "r")
s = string.Template(configfile.read()) s = string.Template(configfile.read())
@ -656,10 +662,9 @@ class AuthManager(object):
port=vpn_port) port=vpn_port)
zippy.writestr(FLAGS.credential_vpn_file, config) zippy.writestr(FLAGS.credential_vpn_file, config)
else: else:
logging.warn("No vpn data for project %s" % LOG.warn("No vpn data for project %s", pid)
pid)
zippy.writestr(FLAGS.ca_file, crypto.fetch_ca(user.id)) zippy.writestr(FLAGS.ca_file, crypto.fetch_ca(pid))
zippy.close() zippy.close()
with open(zf, 'rb') as f: with open(zf, 'rb') as f:
read_buffer = f.read() read_buffer = f.read()
@ -667,38 +672,38 @@ class AuthManager(object):
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
return read_buffer 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""" """Get credential zip for user in project"""
if not isinstance(user, User): if not isinstance(user, User):
user = self.get_user(user) user = self.get_user(user)
if project is None: if project is None:
project = user.id project = user.id
pid = Project.safe_id(project) 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 @staticmethod
def __generate_rc(access, secret, pid): def __generate_rc(access, secret, pid, use_dmz=True, host=None):
"""Generate rc file for user""" """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 = open(FLAGS.credentials_template).read()
rc = rc % {'access': access, rc = rc % {'access': access,
'project': pid, 'project': pid,
'secret': secret, 'secret': secret,
'ec2': FLAGS.ec2_url, 'ec2': '%s://%s:%s%s' % (FLAGS.ec2_prefix,
's3': 'http://%s:%s' % (FLAGS.s3_host, FLAGS.s3_port), cc_host,
FLAGS.cc_port,
FLAGS.ec2_suffix),
's3': 'http://%s:%s' % (s3_host, FLAGS.s3_port),
'nova': FLAGS.ca_file, 'nova': FLAGS.ca_file,
'cert': FLAGS.credential_cert_file, 'cert': FLAGS.credential_cert_file,
'key': FLAGS.credential_key_file} 'key': FLAGS.credential_key_file}
return rc 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 logging
import os import os
import string
import tempfile import tempfile
import zipfile import zipfile
from nova import context from nova import context
from nova import crypto
from nova import db
from nova import exception from nova import exception
from nova import flags from nova import flags
from nova import utils from nova import utils
@ -39,8 +41,17 @@ from nova.api.ec2 import cloud
FLAGS = flags.FLAGS FLAGS = flags.FLAGS
flags.DEFINE_string('boot_script_template', 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') '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): class CloudPipe(object):
@ -48,64 +59,96 @@ class CloudPipe(object):
self.controller = cloud.CloudController() self.controller = cloud.CloudController()
self.manager = manager.AuthManager() self.manager = manager.AuthManager()
def launch_vpn_instance(self, project_id): def get_encoded_zip(self, project_id):
logging.debug("Launching VPN for %s" % (project_id))
project = self.manager.get_project(project_id)
# Make a payload.zip # Make a payload.zip
tmpfolder = tempfile.mkdtemp() tmpfolder = tempfile.mkdtemp()
filename = "payload.zip" filename = "payload.zip"
zippath = os.path.join(tmpfolder, filename) zippath = os.path.join(tmpfolder, filename)
z = zipfile.ZipFile(zippath, "w", zipfile.ZIP_DEFLATED) z = zipfile.ZipFile(zippath, "w", zipfile.ZIP_DEFLATED)
shellfile = open(FLAGS.boot_script_template, "r")
z.write(FLAGS.boot_script_template, 'autorun.sh') 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() z.close()
key_name = self.setup_key_pair(project.project_manager_id, project_id)
zippy = open(zippath, "r") zippy = open(zippath, "r")
context = context.RequestContext(user=project.project_manager, # NOTE(vish): run instances expects encoded userdata, it is decoded
project=project) # 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, def launch_vpn_instance(self, project_id):
# Run instances expects encoded userdata, it is decoded in the LOG.debug("Launching VPN for %s" % (project_id))
# get_metadata_call. autorun.sh also decodes the zip file, hence project = self.manager.get_project(project_id)
# the double encoding. ctxt = context.RequestContext(user=project.project_manager,
user_data=zippy.read().encode("base64").encode("base64"), 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, max_count=1,
min_count=1, min_count=1,
instance_type='m1.tiny', instance_type='m1.tiny',
image_id=FLAGS.vpn_image_id, image_id=FLAGS.vpn_image_id,
key_name=key_name, key_name=key_name,
security_groups=["vpn-secgroup"]) security_group=[group_name])
zippy.close()
def setup_key_pair(self, user_id, project_id): def setup_security_group(self, context):
key_name = '%s%s' % (project_id, FLAGS.vpn_key_suffix) 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: try:
private_key, fingerprint = self.manager.generate_key_pair(user_id, result = cloud._gen_key(context, context.user.id, key_name)
key_name) private_key = result['private_key']
try: 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): if not os.path.exists(key_dir):
os.makedirs(key_dir) os.makedirs(key_dir)
file_name = os.path.join(key_dir, '%s.pem' % key_name) key_path = os.path.join(key_dir, '%s.pem' % key_name)
with open(file_name, 'w') as f: with open(key_path, 'w') as f:
f.write(private_key) f.write(private_key)
except: except:
pass pass
except exception.Duplicate: except exception.Duplicate:
pass pass
return key_name 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. # 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. SSH key_pairs and x509 certificates.
""" """
@ -33,28 +33,57 @@ import utils
import M2Crypto import M2Crypto
from nova import exception from nova import context
from nova import db
from nova import flags from nova import flags
FLAGS = flags.FLAGS FLAGS = flags.FLAGS
flags.DEFINE_string('ca_file', 'cacert.pem', 'Filename of root CA') 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'), flags.DEFINE_string('keys_path', utils.abspath('../keys'),
'Where we keep our keys') 'Where we keep our keys')
flags.DEFINE_string('ca_path', utils.abspath('../CA'), flags.DEFINE_string('ca_path', utils.abspath('../CA'),
'Where we keep our root CA') 'Where we keep our root CA')
flags.DEFINE_boolean('use_intermediate_ca', False, flags.DEFINE_boolean('use_project_ca', False,
'Should we use intermediate CAs for each project?') '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): def ca_folder(project_id=None):
if project_id: if FLAGS.use_project_ca and project_id:
return "%s/INTER/%s/cacert.pem" % (FLAGS.ca_path, project_id) return os.path.join(FLAGS.ca_path, 'projects', project_id)
return "%s/cacert.pem" % (FLAGS.ca_path) 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): def fetch_ca(project_id=None, chain=True):
if not FLAGS.use_intermediate_ca: if not FLAGS.use_project_ca:
project_id = None project_id = None
buffer = "" buffer = ""
if project_id: 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'): def ssl_pub_to_ssh_pub(ssl_public_key, name='root', suffix='nova'):
pub_key_buffer = M2Crypto.BIO.MemoryBuffer(ssl_public_key) buf = M2Crypto.BIO.MemoryBuffer(ssl_public_key)
rsa_key = M2Crypto.RSA.load_pub_key_bio(pub_key_buffer) rsa_key = M2Crypto.RSA.load_pub_key_bio(buf)
e, n = rsa_key.pub() e, n = rsa_key.pub()
key_type = 'ssh-rsa' 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) 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() tmpdir = tempfile.mkdtemp()
keyfile = os.path.abspath(os.path.join(tmpdir, 'temp.key')) keyfile = os.path.abspath(os.path.join(tmpdir, 'temp.key'))
csrfile = os.path.join(tmpdir, 'temp.csr') csrfile = os.path.join(tmpdir, 'temp.csr')
logging.debug("openssl genrsa -out %s %s" % (keyfile, bits)) utils.execute("openssl genrsa -out %s %s" % (keyfile, bits))
utils.runthis("Generating private key: %s", utils.execute("openssl req -new -key %s -out %s -batch -subj %s" %
"openssl genrsa -out %s %s" % (keyfile, bits))
utils.runthis("Generating CSR: %s",
"openssl req -new -key %s -out %s -batch -subj %s" %
(keyfile, csrfile, subject)) (keyfile, csrfile, subject))
private_key = open(keyfile).read() private_key = open(keyfile).read()
csr = open(csrfile).read() csr = open(csrfile).read()
shutil.rmtree(tmpdir) 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): def _ensure_project_folder(project_id):
if not FLAGS.use_intermediate_ca: if not os.path.exists(ca_path(project_id)):
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):
start = os.getcwd() start = os.getcwd()
os.chdir(FLAGS.ca_path) os.chdir(ca_folder())
utils.runthis("Generating intermediate CA: %s", utils.execute("sh geninter.sh %s %s" %
"sh geninter.sh %s" % (intermediate)) (project_id, _project_cert_subject(project_id)))
os.chdir(start) 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): def _sign_csr(csr_text, ca_folder):
tmpfolder = tempfile.mkdtemp() 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.write(csr_text)
csrfile.close() csrfile.close()
logging.debug("Flags path: %s" % ca_folder) logging.debug("Flags path: %s", ca_folder)
start = os.getcwd() start = os.getcwd()
# Change working dir to CA # Change working dir to CA
os.chdir(ca_folder) os.chdir(ca_folder)
utils.runthis("Signing cert: %s", utils.execute("openssl ca -batch -out %s -config "
"openssl ca -batch -out %s/outbound.crt " "./openssl.cnf -infiles %s" % (outbound, inbound))
"-config ./openssl.cnf -infiles %s/inbound.csr" % out, _err = utils.execute("openssl x509 -in %s -serial -noout" % outbound)
(tmpfolder, tmpfolder)) serial = int(out.rpartition("=")[2])
os.chdir(start) os.chdir(start)
with open("%s/outbound.crt" % (tmpfolder), "r") as crtfile: with open(outbound, "r") as crtfile:
return crtfile.read() return (serial, crtfile.read())
def mkreq(bits, subject="foo", ca=0): def mkreq(bits, subject="foo", ca=0):
@ -159,8 +272,7 @@ def mkreq(bits, subject="foo", ca=0):
req = M2Crypto.X509.Request() req = M2Crypto.X509.Request()
rsa = M2Crypto.RSA.gen_key(bits, 65537, callback=lambda: None) rsa = M2Crypto.RSA.gen_key(bits, 65537, callback=lambda: None)
pk.assign_rsa(rsa) pk.assign_rsa(rsa)
# Should not be freed here rsa = None # should not be freed here
rsa = None
req.set_pubkey(pk) req.set_pubkey(pk)
req.set_subject(subject) req.set_subject(subject)
req.sign(pk, 'sha512') req.sign(pk, 'sha512')
@ -224,7 +336,6 @@ def mkcacert(subject='nova', years=1):
# IN THE SOFTWARE. # IN THE SOFTWARE.
# http://code.google.com/p/boto # http://code.google.com/p/boto
def compute_md5(fp): def compute_md5(fp):
""" """
@type fp: file @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): def floating_ip_allocate_address(context, host, project_id):
"""Allocate free floating ip and return the address. """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 @require_context
def floating_ip_allocate_address(context, host, project_id): def floating_ip_allocate_address(context, host, project_id):
authorize_project_context(context, project_id) authorize_project_context(context, project_id)

View File

@ -151,6 +151,16 @@ class Service(BASE, NovaBase):
disabled = Column(Boolean, default=False) 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): class Instance(BASE, NovaBase):
"""Represents a guest vm""" """Represents a guest vm"""
__tablename__ = 'instances' __tablename__ = 'instances'
@ -521,7 +531,7 @@ def register_models():
"""Register Models and create metadata""" """Register Models and create metadata"""
from sqlalchemy import create_engine from sqlalchemy import create_engine
models = (Service, Instance, Volume, ExportDevice, IscsiTarget, FixedIp, models = (Service, Instance, Volume, ExportDevice, IscsiTarget, FixedIp,
FloatingIp, Network, SecurityGroup, FloatingIp, Network, SecurityGroup, Certificate,
SecurityGroupIngressRule, SecurityGroupInstanceAssociation, SecurityGroupIngressRule, SecurityGroupInstanceAssociation,
AuthToken, User, Project) # , Image, Host AuthToken, User, Project) # , Image, Host
engine = create_engine(FLAGS.sql_connection, echo=False) engine = create_engine(FLAGS.sql_connection, echo=False)

View File

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