Add sshCA to the repo
This commit is contained in:
parent
da3bed9677
commit
09f73f8501
|
@ -0,0 +1,10 @@
|
|||
**this is unstable and unfinished software**
|
||||
|
||||
# ssh certificate manager
|
||||
## what is it?
|
||||
This is a simple flask application which manages ssh certificates. It also has a simple api where cloud-init can automatically request certifications.
|
||||
|
||||
## what are ssh certificates
|
||||
Oh no your private key has leaked! Now you need to rekey 100+ computers manually or with ansible if you know how. Another way is to use ssh certificates where there is a PKI set up which signs public keys based on trust. You sign your public key and now every machine wich has the CA in it's list now trusts your signed public key.
|
||||
|
||||
The reverse is also possible and required. The host key can also be signed where you only need to trust the CA. Now every SSH connection signed by a central CA key is trusted and secure. Unless the CA leaks, just store it on a hardware key you dummy.
|
|
@ -0,0 +1,65 @@
|
|||
import sqlite3
|
||||
from db.helpers import *
|
||||
|
||||
class Mydb:
|
||||
def __init__(self):
|
||||
self.conn = sqlite3.connect('test.db', check_same_thread=False)
|
||||
self.cur = self.conn.cursor()
|
||||
|
||||
def insert_ca(self, public, private):
|
||||
query = ''' INSERT INTO CA (date, public, private) VALUES (?, ?, ?) '''
|
||||
data = (time_utc(), public, private)
|
||||
self.cur.execute(query, data)
|
||||
self.conn.commit()
|
||||
return True
|
||||
|
||||
def select_ca(self, caId=1):
|
||||
query = ''' SELECT * FROM CA WHERE caId = ? '''
|
||||
data = (caId, )
|
||||
self.cur.execute(query, data)
|
||||
returned = self.cur.fetchone()
|
||||
|
||||
if not returned:
|
||||
return None
|
||||
|
||||
return returned
|
||||
|
||||
def insert_user(self, username, key, description, signature):
|
||||
query = ''' INSERT INTO users (date, username, key, description, signature) VALUES (?, ?, ?, ?, ?) '''
|
||||
data = (time_utc(), username, key, description, signature)
|
||||
self.cur.execute(query, data)
|
||||
self.conn.commit()
|
||||
return True
|
||||
|
||||
def insert_host(self, principals, key, description, signature):
|
||||
query = ''' INSERT INTO hosts (date, principals, key, description, signature) VALUES (?, ?, ?, ?, ?) '''
|
||||
data = (time_utc(), principals, key, description, signature)
|
||||
self.cur.execute(query, data)
|
||||
self.conn.commit()
|
||||
return True
|
||||
|
||||
def select_hosts(self):
|
||||
query = ''' SELECT * FROM hosts '''
|
||||
data = ()
|
||||
self.cur.execute(query, data)
|
||||
returned = self.cur.fetchall()
|
||||
|
||||
if not returned:
|
||||
return None
|
||||
|
||||
return returned
|
||||
|
||||
def select_users(self):
|
||||
query = ''' SELECT * FROM users '''
|
||||
data = ()
|
||||
self.cur.execute(query, data)
|
||||
returned = self.cur.fetchall()
|
||||
|
||||
if not returned:
|
||||
return None
|
||||
|
||||
return returned
|
||||
|
||||
def __del__(self):
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
|
@ -0,0 +1,7 @@
|
|||
from datetime import datetime
|
||||
|
||||
def time_utc():
|
||||
return datetime.utcnow().isoformat(timespec='seconds') + 'Z'
|
||||
|
||||
def load_time(time):
|
||||
return datetime.fromisoformat(time)
|
|
@ -0,0 +1,103 @@
|
|||
from flask import Flask, render_template, redirect, request, url_for
|
||||
import secrets, requests, os
|
||||
from ssh import ssh_handler
|
||||
from db import conn
|
||||
|
||||
app = Flask(__name__)
|
||||
debug = True
|
||||
|
||||
|
||||
def sign_user(key, username, description):
|
||||
ssh = ssh_handler.SshHandler()
|
||||
ca = db.select_ca()
|
||||
ssh.load_ca(ca[2], ca[3])
|
||||
ssh.load_key(key)
|
||||
try:
|
||||
signature = ssh.sign_user(username, description)
|
||||
db.insert_user(username, key, description, signature)
|
||||
return signature
|
||||
except:
|
||||
return False
|
||||
|
||||
def sign_host(key, principals, description):
|
||||
ssh = ssh_handler.SshHandler()
|
||||
ca = db.select_ca()
|
||||
ssh.load_ca(ca[2], ca[3])
|
||||
ssh.load_key(key)
|
||||
try:
|
||||
signature = ssh.sign_host(principals, description)
|
||||
db.insert_host(principals, key, description, signature)
|
||||
return signature
|
||||
except:
|
||||
return False
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
hosts = db.select_hosts()
|
||||
users = db.select_users()
|
||||
return render_template('index.html', hosts=hosts, users=users)
|
||||
|
||||
@app.route('/api/sign/host', methods=['POST'])
|
||||
def api_sign_host():
|
||||
key = request.form.get('key')
|
||||
principals = request.form.get('principals')
|
||||
description = request.form.get('description')
|
||||
submit = request.form.get('submit')
|
||||
|
||||
if key is None or principals is None or description is None:
|
||||
return 'Missing required parameters', 400
|
||||
|
||||
if len(key) == 0 or len(principals) == 0 or len(description) == 0:
|
||||
return 'Missing required parameters', 400
|
||||
|
||||
r = sign_host(key, principals, description)
|
||||
|
||||
if not r:
|
||||
print(f'{key} -- {principals} -- {description}', flush=True)
|
||||
return 'Error signing key', 500
|
||||
|
||||
if submit == 'Sign':
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return r
|
||||
|
||||
@app.route('/api/sign/user', methods=['POST'])
|
||||
def api_sign_user():
|
||||
key = request.form.get('key')
|
||||
username = request.form.get('username')
|
||||
description = request.form.get('description')
|
||||
submit = request.form.get('submit')
|
||||
|
||||
if key is None or username is None or description is None:
|
||||
return 'Missing required parameters', 400
|
||||
|
||||
if len(key) == 0 or len(username) == 0 or len(description) == 0:
|
||||
return 'Missing required parameters', 400
|
||||
|
||||
r = sign_user(key, username, description)
|
||||
|
||||
if not r:
|
||||
return 'Error signing key', 500
|
||||
|
||||
if submit == 'Sign':
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return r
|
||||
|
||||
|
||||
@app.route('/api/generate/ca', methods=['POST'])
|
||||
def api_generate_ca():
|
||||
ssh = ssh_handler.SshHandler()
|
||||
gen = ssh.generate_ca()
|
||||
db.insert_ca(gen['public'], gen['private'])
|
||||
return 'Ok'
|
||||
|
||||
|
||||
@app.route('/api/print/ca', methods=['GET'])
|
||||
def api_print_ca():
|
||||
return str(db.select_ca()[2].decode('utf-8'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
db = conn.Mydb()
|
||||
app.run(debug=debug, host='0.0.0.0', port=5001)
|
|
@ -0,0 +1,106 @@
|
|||
import subprocess
|
||||
import secrets
|
||||
import os
|
||||
import shutil
|
||||
import cmd
|
||||
import sys
|
||||
|
||||
root = 'D:/sshCA-flask/temp'
|
||||
|
||||
class SshHandler():
|
||||
def __init__(self):
|
||||
self.workfolder = f'{root}/s-{secrets.token_hex(8)}'
|
||||
#self.workfolder = f'{root}/s-bruh'
|
||||
os.mkdir(self.workfolder)
|
||||
|
||||
self.ca = False
|
||||
self.cert = False
|
||||
|
||||
def generate_ca(self, alg='ed25519', comment=secrets.token_hex(8), passphrase=''):
|
||||
if self.ca:
|
||||
return False
|
||||
|
||||
options = ['ssh-keygen',
|
||||
'-q',
|
||||
'-f', f'{self.workfolder}/CA',
|
||||
'-t', alg,
|
||||
'-C', comment,
|
||||
'-N', passphrase]
|
||||
ret = subprocess.check_call(options)
|
||||
|
||||
ca = {}
|
||||
|
||||
with open(f'{self.workfolder}/CA', 'rb') as file:
|
||||
ca['private'] = file.read()
|
||||
|
||||
with open(f'{self.workfolder}/CA.pub', 'rb') as file:
|
||||
ca['public'] = file.read()
|
||||
|
||||
self.ca = True
|
||||
return ca
|
||||
|
||||
def load_ca(self, public, private):
|
||||
if self.ca:
|
||||
return False
|
||||
|
||||
with open(f'{self.workfolder}/CA', 'wb') as file:
|
||||
file.write(private)
|
||||
|
||||
with open(f'{self.workfolder}/CA.pub', 'wb') as file:
|
||||
file.write(public)
|
||||
|
||||
self.ca = True
|
||||
return True
|
||||
|
||||
def load_key(self, cert):
|
||||
if self.cert:
|
||||
return False
|
||||
|
||||
with open(f'{self.workfolder}/key.pub', 'w') as file:
|
||||
file.writelines(cert)
|
||||
|
||||
self.cert = True
|
||||
return True
|
||||
|
||||
def sign_user(self, username, description, validity=3650):
|
||||
if not self.cert or not self.ca:
|
||||
return False
|
||||
|
||||
options = ['ssh-keygen',
|
||||
'-q',
|
||||
'-s', f'{self.workfolder}/CA',
|
||||
'-I', description,
|
||||
'-n', username,
|
||||
#'-V', str(validity),
|
||||
f'{self.workfolder}/key.pub']
|
||||
ret = subprocess.check_call(options)
|
||||
|
||||
with open(f'{self.workfolder}/key-cert.pub', 'r') as file:
|
||||
return file.readline()
|
||||
|
||||
def sign_host(self, hosts, description, validity=3650):
|
||||
if not self.cert or not self.ca:
|
||||
return False
|
||||
|
||||
options = ['ssh-keygen',
|
||||
'-q',
|
||||
'-h',
|
||||
'-s', f'{self.workfolder}/CA',
|
||||
'-I', description,
|
||||
'-n', hosts,
|
||||
#'-V', str(validity),
|
||||
f'{self.workfolder}/key.pub']
|
||||
ret = subprocess.check_call(options)
|
||||
|
||||
with open(f'{self.workfolder}/key-cert.pub', 'r') as file:
|
||||
return file.readline()
|
||||
|
||||
|
||||
def __del__(self):
|
||||
shutil.rmtree(self.workfolder)
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ssh = SshHandler()
|
||||
print(ssh.generate_ca())
|
|
@ -0,0 +1,96 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<style>
|
||||
.ips {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.wide {
|
||||
width: 80%;
|
||||
}
|
||||
input {
|
||||
width: 25%;
|
||||
display: block;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
table tr th {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
</style>
|
||||
<h1>SSH PKI manager</h1>
|
||||
<hr>
|
||||
<h3>Currently issued certificates for <a href="/api/print/ca">CA.pub</a></h3>
|
||||
<table class="ips">
|
||||
<tr>
|
||||
<th>Hostname(s)</th>
|
||||
<th>Issue Date</th>
|
||||
<th>Description</th>
|
||||
<th>Key</th>
|
||||
<th>Signature</th>
|
||||
</tr>
|
||||
{% if not hosts %}
|
||||
<tr>
|
||||
<td><i>No data</i></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for host in hosts %}
|
||||
<tr>
|
||||
<td>{{ host[1] }}</td>
|
||||
<td>{{ host[4] }}</td>
|
||||
<td>{{ host[3] }}</td>
|
||||
<td><input value="{{ host[2] }}" type="text" class="wide"></td>
|
||||
<td><input value="{{ host[5] }}" type="text" class="wide"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Issue Date</th>
|
||||
<th>Description</th>
|
||||
<th>Key</th>
|
||||
<th>Signature</th>
|
||||
</tr>
|
||||
{% if not users %}
|
||||
<tr>
|
||||
<td><i>No data</i></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user[1] }}</td>
|
||||
<td>{{ user[4] }}</td>
|
||||
<td>{{ user[3] }}</td>
|
||||
<td><input value="{{ user[2] }}" type="text" class="wide"></td>
|
||||
<td><input value="{{ user[5] }}" type="text" class="wide"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
<hr>
|
||||
<h3>Sign Host Key</h3>
|
||||
<form method="POST" action="/api/sign/host">
|
||||
<input placeholder="ssh-ed25519 AAAAB3Nza..." type="text" name="key">
|
||||
<input placeholder="iceweazel,iceweazel.example.tld" type="text" name="principals">
|
||||
<input placeholder="Description" type="text" name="description">
|
||||
<input type="submit" value="Sign" name="submit">
|
||||
</form>
|
||||
<hr>
|
||||
<h3>Sign User Key</h3>
|
||||
<form method="POST" action="/api/sign/user">
|
||||
<input placeholder="ssh-ed25519 AAAAB3Nza..." type="text" name="key">
|
||||
<input placeholder="username" type="text" name="username">
|
||||
<input placeholder="Description" type="text" name="description">
|
||||
<input type="submit" value="Sign" name="submit">
|
||||
</form>
|
||||
<hr>
|
||||
<h3>Useful configuration items</h3>
|
||||
<h4>SSHD Config</h4>
|
||||
<p>Paste the following italic in the sshd config</p>
|
||||
<p><i>HostCertificate /etc/ssh/HOSTKEY-cert.pub<br>TrustedUserCAKeys /etc/ssh/CA.pub</i></p>
|
||||
<p>Where HOSTKEY-cert.pub is the signed host certificate and CA.pub is the root CA</p>
|
||||
<h4>SSHD Config</h4>
|
||||
<p>Add the following to the known_hosts file</p>
|
||||
<p><i>@cert-authority LIST-OF-SERVERS ssh-ed25519 AAAAB3Nza.....</i></p>
|
||||
<p>Where the key is the CA and LIST-OF-SERVERS is a csv of hostnames and wildcards, example: hostname.domain.tld,*.ssh.domain.tld</p>
|
Binary file not shown.
Loading…
Reference in New Issue