This codebase implements ssh-agent as a bare bones microservice. This simple codebase allows the use of an SSH key without an end user possessing its private key. This might be useful if:
-
You want to reduce the number of machines that hold a copy of your private key material
-
You don't want a compromised desktop developer machine to compromise private key material
-
You want to perform additional or non-standard authentication of a user before allowing a key to be used - for example you can require a valid TOTP code. This effectively gives you 2FA without having to install extra PAM modules on your entire fleet of machines and without needed to do LDAP 2FA.
-
You have devops / CD deployment tooling and don't really want to commit private keys alongside the configuration for them. (Good doggo).
-
You want to capture an audit event every time a private key is used
-
You want to store private keys for some systems in a more secure environment (different physical security, use of HSM or other hardware crypto, etc) and provide restricted access to them.
-
You want to reduce the likelihood of criticial private key material from been exfiltrated.
-
You have shared cloud infrastructure where each user doesn't have their own account
You will probably have to customize it for your environment - a lot of these ideas are left to the reader (for now).
WARNING: Don't use this when multiple user accounts with multiple SSH keys is more appropriate. It's not a replacement for a good LDAP deployment.
You can get a simple dev environment with Docker and docker-compose. Dev happens on macOS with Docker for Mac:
$ docker-compose -f dev.yml build
$ docker-compose -f dev.yml up
This will automatically run any migrations.
You can run tests with:
$ docker-compose -f dev.yml run --rm flask py.test agentless/
And run flake8 with:
$ docker-compose -f dev.yml run --rm flask flake8 agentless/
You can generate a private key and get the public key with curl:
$ curl -X POST -H "applicaton/json" -d '{"name": "example"}' https://localhost:8000/api/v1/keys
You can use it with your normal SSH client with the cli.py
wrapper:
./cli.py myuser@myserver.org
This will:
- Start an SSH agent running locally
- Update the environment so that subprocesses can see that agent
- Start SSH
- Wait for SSH to finish
- Ensure the SSH agent is cleaned up
You can use it in Paramiko too:
import base64
import struct
import requests
from paramiko.pkey import PKey
def _write_int(data):
return struct.pack(">I", data)
def _write_string(data):
return _write_int(len(data)) + data
class AgentKey(PKey):
def __init__(self, key_id):
self.key_id = key_id
self.agent = agent
self.blob = blob
self.public_blob = None
self.name = Message(blob).get_text()
def asbytes(self):
resp = requests.get(f"http://localhost:8000/api/v1/keys/{self.key_id}").json()
key_type, key_body = resp['public_key'].split(' ')
decoded_body = base64.b64decode(key_body)
return decoded_body
def __str__(self):
return self.asbytes()
def get_name(self):
return "Remote key"
def sign_ssh_data(self, data):
response = requests.post(
f'http://localhost:8000/api/v1/keys/{self.key_id}/sign',
json={'data': base64.b64encode(data).decode('utf-8')}
)
sig = base64.b64decode(response.json()['signature'])
return _write_string(
_write_string(b'ssh-rsa') + _write_string(sig)
)
The ssh-agent protocol is simply an API that allows listing the public keys that are available to participate in authentication and an API for signing challenge payloads. By signing a payload from the host with the private portion of a key it knows about (in ~/.ssh/authorized_keys
) it knows you are who you say you are.
But there is no reason why this has to happen on your machine. If you are suitably authenticated by other means then it's actually nicer to be physically seperated from the private key material.
With agentless, the user installs a client which implements the ssh agent protocol. This client listens on a unix socket and forwards any authentication challenges to agentless via the /api/v1/keys/1/sign
endpoint. This means the local ssh agent client has no access to the secret material.
In Touchdown 0.2.0 back in 2015 we added a simple SSH Agent. This ultimately served two goals:
- The
touchdown ssh
wrapper CLI could launch the full SSH client and use private key credentials that were protected through touchdowns secret infrastructure without them touching disk. - Those secrets could also be forwarded to EC2 instances through paramiko SSH agent forwarding. This meant you could use a bastion host without exposing private key material on the bastion host, and again these secrets were protected through touchdowns secret infrastructure. They are not exposed unencrypted locally, either.
Touchdown could use a mixture of GPG encryption and Amazon KMS - and these could be stacked.
However ultimately the secrets did exist in an unprotected form. If a user was able to run Touchdown then they could extract the secrets and abuse them outside of any Touchdown controls.