Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a448189
Showing
10 changed files
with
515 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
include LICENSE | ||
include README.md | ||
include .gitignore | ||
include ssh_ca_example.conf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 '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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
) |
Oops, something went wrong.