Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
markpeek committed Feb 4, 2014
0 parents commit a448189
Show file tree
Hide file tree
Showing 10 changed files with 515 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .gitignore
@@ -0,0 +1,38 @@
*.py[cod]

# C extensions
*.so

# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox
nosetests.xml

# Translations
*.mo

# Mr Developer
.mr.developer.cfg
.project
.pydevproject

# Vim
*.sw*
23 changes: 23 additions & 0 deletions LICENSE
@@ -0,0 +1,23 @@
Copyright (c) 2014-2014, Bob Van Zant <bob@veznat.com>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
4 changes: 4 additions & 0 deletions MANIFEST.in
@@ -0,0 +1,4 @@
include LICENSE
include README.md
include .gitignore
include ssh_ca_example.conf
100 changes: 100 additions & 0 deletions README.md
@@ -0,0 +1,100 @@
Certificate based SSH
=====================

"*One key to rule them all, One key to find them,
One key to bring them all and in the cloud bind them*"

Certificate based SSH allows us to launch a server at time X and grant
SSH access to that server later at time X + Y without touching the
authorized keys file. Further it allows us to generate certificates that
expire at some predefined time meaning that users can be granted access
to a system for a short period of time.

The primary use case is:

Jane the Engineer needs shell access to a machine running in
production in order to help debug a problem. In general Jane does not
need access to these machines and it is expected that she only needs
access for a few hours at which point her access should automatically
be revoked.

Usage
=====

If you're running this command you must already have access to the
root-ca certificate. Despite being really well encrypted this file is
kept secret and you'll need to pass the "I require access to this file"
test in order to get a copy.

Once you've got the CA file you can use the script here. Usage is found
with the --help option (not documented here to avoid duplicating the
code).

When running this script a number of things happen:

- An entry is made in an audit log in S3 to document that the key was
made, for who, by who and how long the key is valid.
- A serial number is incremented and stored in S3. This makes revoking
certificates later a lot easier.
- The generated certificate is stored in S3 and a temporary (2 hour) URL
is generated for the user to download the certificate

If a user's public key is given as an argument to the script it is also
uploaded to S3 effectively caching it for the next time the script is
used for that user. Without a public key filename being passed in the
script attempts to load the key from S3.

How it works
============

The CA owner creates a new certificate authority keypair. This is just a
generic 4096 bit RSA keypair that could be used for regular old SSH
authentication. However, we will protect the generated private key with our
lives (and a really great 2-factor passphrase).

```
cd ~/.ssh
ssh-keygen -f ssh_ca_production -b 4096
```

We take the public key portion of that key pair and add it to the
authorized_keys file of machines we want to login to. However, unlike
normal, the line in authorized_keys is prefixed with `cert-authority`.

```
echo "cert-authority $(cat user-ca-key.pub)" >> ~/.ssh/authorized_keys
```

At this point the server is ready to accept authentication using any
private key that can also present a certifcate that was signed using the
root-ca's private key.

We now get the users public key and sign it with the CA key. The below command
specifies the S3 bucket (-b), S3 region (-r), environment (-e), user name (-u),
users public key file (-p) and how long before the key expires (-t).

```
sign_key -b my-s3-bucket -r us-west-1 -e production -u user@example.com -p user-example.pub -t +1d
```

The output of this is an S3 URL that you give to the user. The user will now
run `get_key` to download the generated certificate from S3 and install it
into their ~/.ssh directory. Note the quotes around the download link.

```
get_key 'https://my-s3-bucket.s3-us-west-1.amazonaws.com/certs/user%40example.com-cert.pub?Signature=neidfJ5bZ5YbmAi2ouJVZzZzZz%3D&Expires=1391025703&AWSAccessKeyId=AKIAJ7HFYKZIVF3ZZZZ'
```

The user can now log into the remote system using these new keys.

Incompatibilities
=================

Vagrant
-------
When a user has one of these cert keys in their keychain
[vagrant](http://www.vagrantup.com/) will hang in bringing up a new box.
This is due to an incompatibility in the Ruby net-ssh package included in
vagrant. This is being tracked in this
[net-ssh issue](https://github.com/net-ssh/net-ssh/pull/142).

63 changes: 63 additions & 0 deletions scripts/get_cert
@@ -0,0 +1,63 @@
#!/usr/bin/env python
"""Download a given SSH certificate and put it in the right place.
Downloads the certificate specified on argv and then searches through the
user's .ssh directory looking for a matching private key. Puts the certificate
next to that one.
"""
import os
import subprocess
import sys
import tempfile
import urllib


def download_cert_to_tempfile(url):
resp = urllib.urlopen(url)
temp_file = tempfile.NamedTemporaryFile(delete=False)
with temp_file.file:
temp_file.write(resp.read())
return temp_file.name


def get_public_key_fingerprint(cert_path):
proc = subprocess.Popen(['/usr/bin/ssh-keygen', '-L', '-f', cert_path],
stdout=subprocess.PIPE)
for line in proc.stdout.readlines():
if 'Public key:' in line:
fingerprint = line[line.find('RSA-CERT') + 9:]
fingerprint = fingerprint.strip()
return fingerprint


def find_private_key_for_public_key(pub_fingerprint):
ssh_dir = os.getenv('HOME') + '/.ssh'
for filename in os.listdir(ssh_dir):
key_filename = ssh_dir + '/' + filename
if key_filename.endswith('pub'):
continue
proc = subprocess.Popen(['/usr/bin/ssh-keygen', '-l', '-f',
key_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in proc.stdout.readlines():
if pub_fingerprint in line:
return key_filename


def move_cert_into_place(cert_path, private_key_filename):
new_cert_filename = private_key_filename + '-cert.pub'
os.rename(cert_path, new_cert_filename)


if __name__ == '__main__':
if len(sys.argv) != 2:
print 'Usage: %s <URL to cert>' % (sys.argv[0],)
sys.exit(1)

cert_filename = download_cert_to_tempfile(sys.argv[1])
key_fingerprint = get_public_key_fingerprint(cert_filename)
private_key_filename = find_private_key_for_public_key(key_fingerprint)
if not private_key_filename:
print 'Unable to find private key matching certificate.'
sys.exit(1)

move_cert_into_place(cert_filename, private_key_filename)
123 changes: 123 additions & 0 deletions scripts/sign_key
@@ -0,0 +1,123 @@
#!/usr/bin/env python

"""Sign a user's SSH public key.
This script is used to sign a user's SSH public key using a certificate
authority's private key. The signed public key can be presented along with the
user's private key to get access to servers that trust the CA.
The final output of this script is an S3 URL containing the user's signed
certificate. The user needs to take this URL and download the file it points
at. The downloaded file should be named exactly like their private SSH key but
with the suffix "-cert.pub".
For example, if the user's key is ~/.ssh/id_rsa they should do something like
curl <THE URL> > ~/.ssh/id_rsa-cert.pub
"""

import argparse
import ConfigParser
import os
import sys
import tempfile

from contextlib import closing

import ssh_ca
import ssh_ca.s3


if __name__ == '__main__':
default_authority = os.getenv('SSH_CA_AUTHORITY', 's3')
default_config = os.path.expanduser(
os.getenv('SSH_CA_CONFIG', '~/.ssh_ca/config'))

parser = argparse.ArgumentParser(__doc__)
parser.add_argument('-a', '--authority', dest='authority',
default=default_authority, help="Pick one: s3")
parser.add_argument('-c', '--config', dest='config_file',
default=default_config,
help="The configuration file to use. Can also be "
"specified in the SSH_CA_CONFIG environment "
"variable. Default: %(default)s")
parser.add_argument('-e', '--environment', required=True,
help='Environment name')
parser.add_argument(
'-p', help='Path to public key. If set we try to upload this. '
'Otherwise we try to download one.',
dest='public_path')
parser.add_argument(
'-u', help='username / email address', required=True, dest='username')
parser.add_argument(
'--upload', help='Only upload the public key',
dest='upload', action='store_true')
parser.add_argument('-t', '--expires-in', default='+2h',
help="Expires in. A relative time like +1w. Or YYYYMMDDHHMMSS. "
"Default: %(default)s")
args = parser.parse_args()

authority = args.authority
expires_in = args.expires_in
public_path = args.public_path
section = args.environment
upload_only = args.upload
username = args.username

ssh_ca_section = "ssh-ca-" + authority

config = None
if args.config_file:
config = ConfigParser.ConfigParser()
config.read(args.config_file)

# Get a valid CA key file
ca_key = ssh_ca.get_config_value(config, section, 'private_key')
if ca_key:
ca_key = os.path.expanduser(ca_key)
else:
ca_key = os.path.expanduser('~/.ssh/ssh_ca_%s' % (section,))
if not os.path.isfile(ca_key):
print "CA key file %s does not exist." % (ca_key,)
sys.exit(1)

try:
# Create our CA
ca = ssh_ca.s3.S3Authority(config, ssh_ca_section, ca_key)
except ssh_ca.SSHCAInvalidConfiguration, e:
print "Issue with creating CA: %s" % e.message
sys.exit(1)

if upload_only:
if not public_path:
print "Upload needs a public key specified."
sys.exit(1)
ca.upload_public_key(username, public_path)
print "Public key %s for username %s uploaded." % (public_path,
username)
sys.exit(0)

# Figure out if we use a local new public key or an existing one
if public_path:
ca.upload_public_key(username, public_path)
delete_public_key = False
else:
public_key_contents = ca.get_public_key(username)
if public_key_contents is None:
print"Key for user %s not found." % (username)
sys.exit(1)
(fd, public_path) = tempfile.mkstemp()
with closing(os.fdopen(fd, 'w')) as f:
f.write(public_key_contents)
delete_public_key = True

# Sign the key
cert_contents = ca.sign_public_key(public_path, username, expires_in)

print
print 'Public key signed, certificate available for download here:'
print ca.upload_public_key_cert(username, cert_contents)

if delete_public_key:
os.remove(public_path)
17 changes: 17 additions & 0 deletions setup.py
@@ -0,0 +1,17 @@
import glob
from setuptools import setup

setup(
name='ssh-ca',
version='0.1.0',
description="SSH CA utilities",
author="Bob Van Zant",
author_email="bob@veznat.com",
maintainer="Mark Peek",
maintainer_email="mark@peek.org",
url="https://github.com/cloudtools/ssh-ca",
license="New BSD license",
packages=['ssh_ca'],
scripts=glob.glob('scripts/*'),
use_2to3=True,
)

0 comments on commit a448189

Please sign in to comment.