This repository contains a serverless Certificate Authority that can be used to provide signed certificates for SSH access running on Cloudflare Workers.
The solutions comprises of the CA running as a Worker, a Go based client and an external OIDC IdP (this not provided as part of this solution).
The IdP may be any OIDC compatible service that returns a JWT with at least
an email claim in the OIDC access token, however at this time only
Cloudflare Access has been tested.
The flow to obtain a User SSH certificate using the CLI version is as follows:
- The user initiates
ssh-ca-client-cli login - If required a new SSH key is generated and the a browser is opened to
visit
http://localhost:3000/auth/login - The client redirects the user to the configured IdP
- The IdP returns the user to the callback URL (default
http://localhost:3000/auth/callback) - The client uses the JWT from the IdP as
Authorization: Bearer <TOKEN>in aPOSTrequest containing the users SSH public key to the CA's/api/v2/certificateendpoint - The CA verifies the incoming JWT and assuming it is valid and verified, will respond with a signed certificate based on the provided public key
- The client saves the certificate and adds the SSH private key and certificate to the local SSH Agent.
This flow is shown below once the user executes ssh-ca-client-cli login:
sequenceDiagram
User->>Client: GET /auth/login
activate Client
Client-->>User: Redirect to IdP
deactivate Client
activate User
User->>IdP: OIDC authentication flow
deactivate User
activate IdP
IdP-->>User: Redirect to Client
deactivate IdP
activate User
User->>Client: Completes OIDC redirect
deactivate User
activate Client
activate Client
Client-->>User: Auth flow completed
deactivate Client
Client->>CA: POST /api/v2/certificate
deactivate Client
activate CA
CA-->>Client: Signed certificate
deactivate CA
activate Client
Client->>SSH Agent: Add key and certificate to Agent
deactivate Client
If a refresh token is available, the process looks like this when running
ssh-ca-client-cli login:
sequenceDiagram
Client->>IdP: Request authentication token
activate IdP
IdP-->>Client: Token returned
deactivate IdP
activate Client
Client->>CA: POST /api/v2/certificate
deactivate Client
activate CA
CA-->>Client: Signed certificate
deactivate CA
activate Client
Client->>SSH Agent: Add key and certificate to Agent
deactivate Client
If the process to request a new authentication token fails using the refresh token, the standard authentication process is followed which requires user interaction.
The process to request an intitial Host certificate is similar to obtaining
a User certificate once ssh-ca-client-cli host is run, however instead of
adding the obtained certificate to a SSH Agent, it is written to
<KEYPATH>-cert.pub instead:
sequenceDiagram
User->>Client: GET /auth/login
activate Client
Client-->>User: Redirect to IdP
deactivate Client
activate User
User->>IdP: OIDC authentication flow
deactivate User
activate IdP
IdP-->>User: Redirect to Client
deactivate IdP
activate User
User->>Client: Completes OIDC redirect
deactivate User
activate Client
activate Client
Client-->>User: Auth flow completed
deactivate Client
Client->>CA: POST /api/v2/host/request
deactivate Client
activate CA
CA-->>Client: Signed certificate
deactivate CA
activate Client
Client->>Writes Certificate: Certificate written to disk
deactivate Client
Renewals are done using the existing certificate and will succeed if:
- The certificate is not expired
- It was issued by the CA
- The public key used to obtain the certificate is the same as the one currently presented
This means the OIDC authentication flow is not required in this case as shown
below when ssh-ca-client-cli host --renew is run:
sequenceDiagram
Client->>CA: POST /api/v2/host/renew
activate CA
CA-->>Client: Signed certificate
deactivate CA
activate Client
Client->>Writes Certificate: Certificate written to disk
deactivate Client
Once you have cloned this repository, firstly install the dependencies:
npm install- Edit the variables in
wrangler.jsonc:
- Add the private key for your SSH CA to your Cloudflare Secrets Store:
"secrets_store_secrets": [
{
"binding": "PRIVATE_KEY",
// The ID of the secret store
"store_id": "<secret store id>",
// The name of the secret
"secret_name": "<secret name>"
}
]The secret should be an OpenSSH private key generated as follows:
ssh-keygen -t ecdsa -b 256 -f path/to/ca_keyAt this time only ECDSA and ED25519 key types are supported for the CA, however RSA, ECDSA and ED25519 keys are supported for users and hosts.
- Generate the Worker types for your deployment:
npm run cf-typegen- Deploy your Worker:
npm run deployThis example shows the configuration in Cloudflare Access, however other OIDC IdP's should be generally equivalent:
The Redirect URL must match the configured value in the client.
Transfer the IdP settings as follows:
The openid and email scopes are required, with enabling of refresh tokens
(the offline_access scope) being optional, but recommended.
The client requires a configuration file that defines the details of the OIDC IdP and where to find the SSH CA as follows:
issuer: OIDC Issuer
client_id: OIDC Client ID
scopes: ["openid", "email", "profile"]
redirect_url: http://localhost:3000/auth/callback
ca_url: https://ssh-ca.example.com/
# Providing the public key of the CA is optional but recommended so the response can be verified
trusted_ca: ecdsa-sha2-nistp256 ....The default location of this configuration file is $HOME/.ssh-serverless-ca/config.yml
however this can be overridden using the --config command line option.
Please download the client for your OS from the releases page.
Assuming a local SSH agent is running, the client can be started as follows:
# generate a SSH private key
ssh-ca-client-cli generate
# perform a login to the IdP and request a signed certificate
ssh-ca-client-cli login
# issue host keys
ssh-ca-client-cli hostThis should automatically start a web browser to initiate the OIDC login flow,
if not you may manually visit http://localhost:3000/auth/login to start this
process.
If provided by the OIDC IdP the refresh token will be saved for subsequent
ssh-ca-client login invocations and an attempt will be made to obtain a new
auth token using the saved refresh token.
The users private key, the most recently issued certificate and the OIDC refresh token (if available) are written to a user specific configuration file for subsequent use.
Sensitive material such as the SSH private key and OIDC refresh token are encrypted on Windows using the Data Protection API (DPAPI) so the values are only decryptable by the same user that originally encrypted it.
Although access to the config file by another user is possible, the values cannot be read (assuming DPAPI is secure).
On Linux a random key is generated and saved in the users login keyring
which is then used to encrypt this sensitive material using AES-GCM encryption.
On other platforms, this is not the case and this data is simply stored as BASE64 encoded strings, so security is less than ideal and filesystem permissions must be used to prevent unauthorised access.
For systems to allow SSH login using certiifcates the following configuration changes must be made:
PubkeyAuthentication yes
TrustedUserCAKeys /etc/ssh/ca.pub
AuthorizedPrincipalsFile /etc/ssh/principals.d/%u
# Uncomment the following lines to enable host certificates
# HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub
# HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub
# HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
The contents of /etc/ssh/ca.pub is the public key of the SSH CA, which can be
retrieved as follows:
curl https://ssh-ca.example.com/api/v2/ca | sudo tee /etc/ssh/ca.pubThe /etc/ssh/principals.d directory should contain a file corresponding to
a local user that contains a list of principals that should be allowed
login.
Using the principals list in SSH_CERTIFICATE_PRINCIPALS above, the
following file named /etc/ssh/principals.d/admin would allow login
as the SSH user admin for the bearer of an issued (and valid) certificate:
ssh-admin
The icons used by the client are made by Freepik from www.flaticon.com.

