1
Fork 0

Add sshCA to the repo

This commit is contained in:
Ventilaar 2023-01-28 23:37:10 +01:00
parent da3bed9677
commit 09f73f8501
9 changed files with 387 additions and 0 deletions

10
sshCA-flask/README.md Normal file
View File

@ -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.

View File

65
sshCA-flask/db/conn.py Normal file
View File

@ -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()

View File

@ -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)

103
sshCA-flask/main.py Normal file
View File

@ -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)

View File

View File

@ -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())

View File

@ -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>

BIN
sshCA-flask/test.db Normal file

Binary file not shown.