Skip to content

Instantly share code, notes, and snippets.

@jameshi16
Last active August 7, 2022 01:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jameshi16/7c8cc538a98ba34ef92af063977fe490 to your computer and use it in GitHub Desktop.
Save jameshi16/7c8cc538a98ba34ef92af063977fe490 to your computer and use it in GitHub Desktop.
[Mine] Easily introduce new machines to ~/..ssh/authorized_keys

SSH Introducing Scripts

I find it a huge pain to introduce new machines as authorized hosts when linking them up to my existing servers. While enterprise-level systems exists for sysadmins to manage thousands of SSH Keys, I am but a simple man with his hoard of servers.

Hence, I created these scripts to alleviate the pain & hassle of adding a new hosts to my servers.

Features

  • Simple UI to upload keys
  • Random URL generated for key upload all the time
  • Has TLS to prevent MITM (verify fingerprint before connecting)

Usage

You will need:

  • A host that can already connect to the SSH Server (introducer)
  • A host with the SSH Server (server)
  • A host to introduce to the SSH Server (host)

First, SSH into your server from your introducer.

Set-up Phase (do once)

Download upload-public-key.py into your server, and make it executable:

wget https://gist.githubusercontent.com/jameshi16/7c8cc538a98ba34ef92af063977fe490/raw/0f242d2fbc448e5286e4f36e274e3c06e79d33d6/upload-public-key.py \
chmod +x ./upload-public-key.py

Download update_authorized_keys.sh into your server under ~/.ssh/, and also make it executable:

wget -O ~/.ssh/update_authorized_keys.sh https://gist.githubusercontent.com/jameshi16/7c8cc538a98ba34ef92af063977fe490/raw/0f242d2fbc448e5286e4f36e274e3c06e79d33d6/update_authorized_keys.sh \
chmod +x ~/.ssh/update_authorized_keys.sh

If you are using the same folders as I do, create the public_keys/ssh_keys folder hierarchy:

mkdir -p ~/public_keys/ssh_keys

Paste all of your existing public keys into the directory (i.e. in my case, ~/public_keys/ssh_keys).

Key uploading phase

Run ./upload_public_key.py -i <ip address> on your server from your introducer. For security purposes, you must choose an IP address to expose the server to (either use an internal IP address if you are in that environment, or find out your server's public IP address with curl ifconfig.me)

You should see something like the following echo'd:

Certificate location: /tmp/tmpec8k2a3w
Keyfile location: /tmp/tmpxs4j0x68
Server listening on: <ip>:43529
Certificate fingerprint: 17:9B:3A:A3:2D:CB:82:B3:CF:43:77:D8:FE:82:3B:EA:69:94:C3:65
GET URL: https://<ip>:43529/rvFmnar-UQw
POST URL: https://<ip>:43529/cDFObUte70SA_2KIFjja8Q

On the host, generate a public/private keypair. Then, navigate to the URL stated in GET, and upload the .pub portion fo the keypair. Name the key as if you will be renaming the file to it, for example, typing in ubuntu-laptop will save the uploaded key as ubuntu-laptop.pub.

Upon successful submission, the script on server will automatically quit. Navigate to ~/.ssh and run ./update_authorized_keys.sh. If successful, the new keys will be added into the authorized_keys file.

#!/bin/bash
# Updates the ~/.authorized_keys file based on the public keys file
mv authorized_keys authorized_keys.backup
touch authorized_keys
# Modify the below line to your requirements
cat $HOME/public_keys/ssh_keys/* > authorized_keys
chmod 660 authorized_keys
rm authorized_keys.backup
#!/usr/bin/env python3
# upload-public-key.py
# Exposes a randomly generated URL for anyone in the network to upload a public key for SSH access
import cgi, getopt, secrets, socket, ssl, sys, tempfile
from http.server import BaseHTTPRequestHandler, HTTPServer
from OpenSSL import crypto, SSL
from pathlib import Path
# Help text
help_text = """Usage: {:s} -i <ip address> [-p <port>]
-h Displays this text
-i IP address for this script to listen to
-p Port address for this script to listen to
The script will write the uploaded key directly to ~/.ssh, where ~ is the $HOME variable. If port is left empty, a random one will be assigned. The script will output the following things:
- Address the script is listening to
- Fingerprint of the TLS certificate used for HTTPS
- Address the client should be connecting to for upload
"""
# HTML template
upload_html_template = """
<html><head>
<title>SSH Upload Page</title>
</head>
<body>
<h1>Excuse the brevity, but please upload the public key.</h1>
<form action="{:s}" method="post" enctype="multipart/form-data">
Choose file to upload:
<input type="file" name="file" id="file"><br>
Call it something:
<input type="text" name="name" id="name">
<input type="submit" value="Submit" name="submit">
</form>
</body></html>
"""
# Destination folder (modify to your requirements)
dest = str(Path.home()) + '/public_keys/ssh_keys'
# Other main-level context variables
certfile = tempfile.NamedTemporaryFile()
keyfile = tempfile.NamedTemporaryFile()
random_get_path = secrets.token_urlsafe(8)
random_post_path = secrets.token_urlsafe(16)
# Function to generate a random certificate
def generate_certificate(ip_addr):
k = crypto.PKey()
k.generate_key(crypto.TYPE_RSA, 4096)
cert = crypto.X509()
cert.get_subject().C = 'US'
cert.get_subject().ST = 'Somewhere'
cert.get_subject().L = 'Over the rainbow'
cert.get_subject().O = 'Some Organisation'
cert.get_subject().OU = 'SO'
cert.get_subject().CN = ip_addr
cert.get_subject().emailAddress = 'me@noreply.com'
cert.set_serial_number(0)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(300) # expires in 5 minutes
cert.set_issuer(cert.get_subject())
cert.set_pubkey(k)
cert.sign(k, 'sha512')
certfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
keyfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
certfile.flush()
keyfile.flush()
return cert.digest('sha1')
# Function to parse address from cmd args
def parse_address_from_args(argv):
ip = ''
port = 0
try:
opts, args = getopt.getopt(argv[1:], "hi:p:", ["ip=","port="])
except getopt.GetoptError:
print(help_text.format(argv[0]))
sys.exit(1)
for opt, arg in opts:
if opt == '-h':
print(help_text.format(argv[0]))
sys.exit(0)
elif opt in ('-i', '--ip'):
ip = arg
elif opt in ('-p', '--port'):
port = int(arg)
if ip == '':
print("Missing IP address. Cannot continue.")
print(help_text.format(argv[0]))
sys.exit(1)
return (ip, port)
# HTTP Handler
class SSHKeyUploadHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != '/' + random_get_path:
print('Warning: GET does not match expected URL: ' + self.path)
self.send_response_only(204)
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(upload_html_template.format('/' + random_post_path).encode())
def do_POST(self):
if self.path != '/' + random_post_path:
print('Warning: POST does not match expected URL: ' + self.path)
ctype, pdict = cgi.parse_header(self.headers['Content-Type'])
pdict['boundary'] = bytes(pdict['boundary'], 'utf-8')
pdict['CONTENT-LENGTH'] = int(self.headers['Content-length'])
if ctype == 'multipart/form-data':
form = cgi.parse_multipart(self.rfile, pdict)
if 'name' not in form or 'file' not in form:
print('Warning: POST data is misformed')
self.send_response_only(204)
try:
filename = ''
data = None
if isinstance(form['name'], list):
filename = form['name'][0]
else:
filename = form['name']
if isinstance(form['file'], list):
data = form['file'][0]
else:
data = form['file']
print(data)
print('Writing to {:s}/{:s}.pub'.format(dest, filename))
with open('{:s}/{:s}.pub'.format(dest, filename), 'wb') as output_file:
output_file.write(data)
except IOError:
print('IOError encountered')
self.send_response_only(204)
self.send_response_only(200, 'OK. Run update_authorized_keys.sh to finalize')
print('Saved key: {:s}'.format(filename))
sys.exit(0)
if __name__ == '__main__':
address = parse_address_from_args(sys.argv)
print('Certificate location: {:s}'.format(certfile.name))
print('Keyfile location: {:s}'.format(keyfile.name))
fingerprint = generate_certificate(address[0])
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile.name, keyfile.name)
httpd = HTTPServer(address, SSHKeyUploadHandler)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
actual_address = httpd.socket.getsockname()
print('Server listening on: {:s}:{:d}'.format(actual_address[0], actual_address[1]))
print('Certificate fingerprint: {:s}'.format(fingerprint.decode()))
print('GET URL: https://{:s}:{:d}/{:s}'.format(actual_address[0], actual_address[1], random_get_path))
print('POST URL: https://{:s}:{:d}/{:s}'.format(actual_address[0], actual_address[1], random_post_path))
httpd.serve_forever()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment