Certificate-based SSH authentication

Why?

Certificate-based SSH authentication is superior to SSH keys in many ways;

  • SSH certificates intrinsically possess a validity period before and after which they are invalid for providing authentication.
  • SSH certificates can be embedded with SSH restrictions that limit:
    • Who can use the certificate
    • Which SSH client machines can use the certificate
    • The list of available SSH features (X11Forwarding, AgentForwarding, etc)
    • Commands that can be run via SSH
    • and more!

Requirements

  • Systems running SSH
    • OpenSSH 5.4 or higher
    • Ruby
      • rest-client
    • NTP
      • Time synchronization is strictly speaking optional, but highly recommended especially for certifications that are short-lived.
  • API service running on CA
    • Ruby
      • Sinatra

Setup

For the purposes of this document, let’s consider three systems:

  • Certification Authority
    • System name “ca
    • Will host our Certification Authority
    • Red Hat Enterprise Linux 7
  • Server #1
    • System name “server1
    • Will function as both an SSH client and server
    • Red Hat Enterprise Linux 7
  • Server #2
    • System name “server2
    • Will function as both an SSH client and server
    • Ubuntu 16.04

1. Install Software

Let’s ensure that OpenSSH is installed, and is a compatible version for our purposes.

[admin@ca ~]$ rpm -qa | grep -i openssh
openssh-6.4p1-8.el7.x86_64
openssh-server-6.4p1-8.el7.x86_64
openssh-clients-6.4p1-8.el7.x86_64

That should do it for ca, let’s ensure we’ve got the necessary software on server1 and server2.  For these systems, we’ll only require the OpenSSH client and server packages.

[admin@server1 ~]$ rpm -qa | grep -i openssh
openssh-clients-6.4p1-8.el7.x86_64
openssh-6.4p1-8.el7.x86_64
openssh-server-6.4p1-8.el7.x86_64

and

[admin@server2 ~]$ rpm -qa | grep -i openssh
openssh-clients-6.4p1-8.el7.x86_64
openssh-6.4p1-8.el7.x86_64
openssh-server-6.4p1-8.el7.x86_64

We’re all set.

2. SSH Host Certificates

As you are likely already aware, in SSH there are two types of SSH keys – host keys and user keys.  Host keys are used to establish the identity of the host to remote SSH clients, while user keys are used to establish identity users to remote SSH servers.  We’re going to need to set up certificates based on both the host and user keys – but we can validate our configuration part way through if we start with host keys… so let’s start with them.

Create a host CA key pair

On ca, use ssh-keygen to create a host CA key pair.

[root@ca ~]# mkdir /etc/ssh_ca
[root@ca ~]# chmod 700 /etc/ssh_ca
[root@ca ~]# cd /etc/ssh_ca
[root@ca ssh_ca]# ssh-keygen -q -b 4096 -f host_ca
Enter passphrase (empty for no passphrase): secretHostPassphrase
Enter same passphrase again: secretHostPassphrase
[root@ca ssh_ca]# ls -al
total 20
drwx------. 2  root root   38 Feb 24 11:47 .
drwxr-xr-x. 87 root root 8192 Feb 24 11:47 ..
-rw-------. 1  root root 3326 Feb 24 11:47 host_ca
-rw-r--r--. 1  root root  733 Feb 24 11:47 host_ca.pub

Options explanation:

  • -q
    • This suppresses all output except for that which is necessary.
  • -b 4096
    • Creates a key pair where each key is 4096 bits in length
    • Overkill? maybe.
  • -f host_ca
    • The name of our certification authority’s host key pair.
    • /etc/ssh_ca/host_ca will contain the private key.
    • /etc/ssh_ca/host_ca.pub will contain the public key.

The passphrase used here will be required anytime a new host key needs to be signed.  Don’t lose it.

Sign the CA’s Host RSA key

Now since OpenSSH is already installed it’s very likely that a variety of host SSH keys already exist.  On RHEL 7 these keys can be found in /etc/ssh.

[root@ca ssh_ca]# ls -al /etc/ssh/ssh_host*
-rw-r-----. 1 root ssh_keys  227 Feb 24 08:35 /etc/ssh/ssh_host_ecdsa_key
-rw-r--r--. 1 root root      162 Feb 24 08:35 /etc/ssh/ssh_host_ecdsa_key.pub
-rw-r-----. 1 root ssh_keys 1675 Feb 24 08:35 /etc/ssh/ssh_host_rsa_key
-rw-r--r--. 1 root root      382 Feb 24 08:35 /etc/ssh/ssh_host_rsa_key.pub

Let’s take the host RSA public key (/etc/ssh/ssh_host_rsa_key.pub) and sign it with our host_ca private key.

[root@ca ssh_ca]# ssh-keygen -s host_ca \
                             -I host_ca.fabrikam.com \
                             -h \
                             -n ca,ca.fabrikam.com \
                             -V +52w \
                             /etc/ssh/ssh_host_rsa_key.pub
Enter passphrase: secretHostPassphrase
Signed host key /etc/ssh/ssh_host_rsa_key-cert.pub: id "host_ca.fabrikam.com"
serial 0 for ca,ca.fabrikam.com valid from 2017-02-24T11:55:00 to 
2018-02-23T11:56:53

Options explanation:

  • -s host_ca
    • The file name of the host private key to use for signing.
  • -I host_ca.fabrikam.com
    • The key identifier to include in the certificate.
  • -h
    • Generate a host certificate (instead of a user certificate)
  • -n ca,ca.fabrikam.com
    • The principal names to include in the certificate.
    • For host certificates this is a list of all names that the system is known by.
    • Note: Use the unqualified names carefully here in organizations where hostnames are not unique (ca.fabrikam.com vs. ca.dev.fabrikam.com)
  • -V +52w
    • The validity period.
    • For host certificates, you’ll probably want them pretty long lived.
    • This setting sets the validity period from now until 52 weeks hence.
  • /etc/ssh/ssh_host_rsa_key.pub
    • The name of the host RSA public key to sign.
    • Our signed host key (certificate) will be /etc/ssh/ssh_host_rsa_key-cert.pub.

It’s time now to tell the SSH daemon about this host certificate.  There are three steps here;

  • Modify the SSH daemon’s configuration file to be aware of this new RSA host certificate.
  • Add a system-wide known_hosts file with the our host_ca’s public key so that the SSH daemon can validate certificates.
  • Restart/reload the SSH daemon.

The SSH daemon’s configuration file is /etc/ssh/sshd_config, let’s add the following line and then force the SSH daemon to reload it’s configuration file.

...
#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

# The default requires explicit activation of protocol 1
#Protocol 2

# HostKey for protocol version 1
#HostKey /etc/ssh/ssh_host_key
# HostKeys for protocol version 2
HostKey /etc/ssh/ssh_host_rsa_key
#HostKey /etc/ssh/ssh_host_dsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key

### Host certificate
HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub

# Lifetime and size of ephemeral version 1 server key
#KeyRegenerationInterval 1h
#ServerKeyBits 1024
...

Now it’s time to create the system-wide known_hosts file.   The system-wide known_hosts file is called /etc/ssh/ssh_known_hosts.  In this file we are going to place the contents of our host_ca.pub file, and some other data.  This is what it should look like:

@cert-authority *.fabrikam.com ssh-rsa AAAAB3[...redacted...]lZj3r3xTOaBJ

Where everything highlighted has been copied from the /etc/ssh_ca/host_ca.pub file.

Now it’s time to send the SSH daemon the hangup signal to tell it to reload the configuration file.

[root@ca etc]# kill -HUP `pidof sshd`

Sign the RSA Host key for Server1, Server2, … Servern

Now it’s time to repeat the same processes (above), namely:

  • Collect host keys from different servers running SSHD.
  • Sign the server’s RSA host public keys
  • Create the /etc/ssh/ssh_known_hosts file
  • Copy the SSH host certificate to their appropriate locations
  • Modify the SSHD configuration file on each server
  • Restart the SSH daemon

To do this, you’ll need to copy the RSA host public key from the server back to ca, sign it, copy the host certificate back to the server, modify the SSHD configuration file on the server, copy the system-wide known_hosts file from ca to the server, and restart SSHD.

3. SSH User Certificates

In a similar way that host certificates uniquely identify SSH hosts to SSH clients, user certificates are used to uniquely identify SSH users to remote SSH daemons.

Before we begin, if you don’t already have an SSH user key, it’s time to create one.

[admin@vm-ca ~]$ ssh-keygen -t rsa -b 2048
Enter file in which to save the key (/home/admin/.ssh/id_rsa):
Enter passphrase (empty for no passphrase): secretAdminPassphrase
Enter same passphrase again: secretAdminPassphrase

We’re going to need this key pair later, but it’s not technically part of the process.

Let’s begin…

On ca, use ssh-keygen to create a user CA key pair.

[root@ca ~]# cd /etc/ssh_ca
[root@ca ssh_ca]# ssh-keygen -q -b 4096 -f user_ca
Enter passphrase (empty for no passphrase): secretUserPassphrase
Enter same passphrase again: secretUserPassphrase
[root@ca ssh_ca]# ls -al
total 20
drwx------. 2  root root   38 Feb 24 11:47 .
drwxr-xr-x. 87 root root 8192 Feb 24 11:47 ..
-rw-------. 1  root root 3326 Feb 24 11:47 host_ca
-rw-r--r--. 1  root root  733 Feb 24 11:47 host_ca.pub
-rw-------. 1  root root 3326 Feb 24 11:59 user_ca
-rw-r--r--. 1  root root  733 Feb 24 11:59 user_ca.pub

Options explanation:

  • -q
    • This suppresses all output except for that which is necessary.
  • -b 4096
    • Creates a key pair where each key is 4096 bits in length
    • Overkill? maybe.
  • -f user_ca
    • The name of our certification authority’s user key pair.
    • /etc/ssh_ca/user_ca will contain the private key.
    • /etc/ssh_ca/user_ca.pub will contain the public key.

Now, let’s take our admin user’s  RSA public key (/home/admin/.ssh/id_rsa_key.pub) and sign it with our user_ca private key.

[root@ca ssh_ca]# ssh-keygen -s user_ca \
                             -I user_admin \
                             -n admin \
                             -V +24h \
                             /home/admin/.ssh/id_rsa.pub
Enter passphrase: secretUserPassphrase
Signed host key /home/admin/.ssh/id_rsa-cert.pub: id "user_admin"
serial 0 for admin valid from 2017-02-29T9:12:00 to 
2018-03-01T9:12:00

Options explanation:

  • -s user_ca
    • The file name of the host private key to use for signing.
  • -I user_admin
    • The key identifier to include in the certificate.
  • -n admin
    • The principal names to include in the certificate.
    • For user certificates this is a list of all usernames that this certificate should include for authentication purposes.
  • -V +24h
    • The validity period.
    • For user certificates, you’ll probably want them pretty short lived, once an SSH session is authenticated the certificate can safely expire without impacting the established session.
    • This setting sets the validity period from now until 24 hours hence.
  • /home/admin/.ssh/id_rsa.pub
    • The name of the user RSA public key to sign.
    • Our user certificate will be /home/admin/.ssh/id_rsa-cert.pub.

orange-warning-icon-3 It should be mentioned here that if you create an SSH certificate as root for another user, you’ll likely need to chown the resulting certificate so that the rightful owner owns their own certificate.

Now it’s time to create a system-wide SSH known_hosts file (/etc/ssh/ssh_known_hosts).   We are going to place the contents of our host_ca.pub file, and some other data.  This is what it should look like:

@cert-authority *.fabrikam.com ssh-rsa AAAAB3[...redacted...]lZj3r3xTOaBJ

Where everything highlighted has been copied from the /etc/ssh_ca/host_ca.pub file.

In the same way that we had to tell the SSH daemon about the host_ca public key, we need to do the same thing (in a different way) about the user_ca public key.

It’s time now to tell the SSH daemon about the certification authority’s public key.  There are three steps here;

  • Modify the SSH daemon’s configuration file to point the user_ca.pub key.
  • Restart/reload the SSH daemon.

The SSH daemon’s configuration file is /etc/ssh/sshd_config, let’s add the following line and then force the SSH daemon to reload it’s configuration file.

...
#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

# The default requires explicit activation of protocol 1
#Protocol 2

# HostKey for protocol version 1
#HostKey /etc/ssh/ssh_host_key
# HostKeys for protocol version 2
HostKey /etc/ssh/ssh_host_rsa_key
#HostKey /etc/ssh/ssh_host_dsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key

### Host certificate
HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub

### User CA certificate 
TrustedUserCAKeys /etc/ssh/user_ca.pub 

# Lifetime and size of ephemeral version 1 server key 
#KeyRegenerationInterval 1h 
#ServerKeyBits 1024 
...

Finally, send the SSH daemon the hangup signal to tell it to reload the configuration file.

[root@ca etc]# kill -HUP `pidof sshd`

Sign the User key for User1, User2, … Usern

Now it’s time to repeat the same processes (above), namely:

  • Collect user SSH public keys from users (including those on different servers)
  • Sign each user public key with the CA user private key
  • Copy the user certificates to their appropriate locations
  • Modify the SSHD configuration file on the various servers.
  • Restart the SSH daemon

To do this, copy each user’s SSH public key from the server back to ca, sign it, copy the user certificate back to the user’s ~/.ssh/ folder on the appropriate server, modify the SSHD configuration file on the server, and restart SSHD.

orange-warning-icon-3 Troubleshooting tips:

  • Use the (-v) verbose option on the SSH client command line.
[admin@server1 ~]$ ssh -v admin@server2
  • Tail the authentication log on the server running the SSH daemon
[root@server2 ~]# tail -f /var/log/auth.log
  • Ensure that all systems involved have synchronized system clocks.  For short-lived signed SSH certificates, unsynchronized clocks may cause certificates to prematurely expire, or be not-yet-valid.

4. API Automation

As mentioned above there are a lot of reasons to adopt certificate-based SSH authentication.  The most compelling reason to use SSH certificates is because it transforms the immortal SSH key into something that implicitly has a finite lifespan.  In short, SSH certificates do not need to have their lifecycle managed – their lifespan is implicitly baked into them.  By setting the lifespan of SSH certificates to 2 minutes, they become single-use disposable SSH keys.

To achieve this nirvana though, there is a cost.  The cost comes in the form of authoring a robust API (and client) to securely sign user and host SSH public keys.

Check out our proof-of-concept Ruby API client and service on GitHub:

Ruby REST API (api.rb)

Ruby REST Client (keymgr.rb)

orange-warning-icon-3 This code is not intended to be a turn-key solution.  It is intended to demonstrate functionality in a proof-of-concept, non-production setting.


#!/usr/bin/ruby
require 'rest-client'
require 'socket'
require 'base64'
require 'json'
require 'digest'
def usage()
puts
puts "Usage:"
puts " ruby keymgr.rb ( -u [userid2][,userid3]…| -d | -h fqdn )"
puts
puts " -u [userid2][,userid3]…"
puts " Request user public key signing for the current user (with other user principles too)"
puts " -d"
puts " Request that SSH daemon files (known_hosts and sshd_config) be updated (requires root)"
puts " -h fqdn"
puts " Requests host public key signing (requires root)"
puts
end
# Base part of our API's URI
base_api_uri = "http://vm-ca.fabrikam.com:4567/"
# The list of routes supported by this API client
host_sign_route = "sign_host_key"
user_sign_route = "sign_user_key"
daemon_update_route = "daemon_update"
# Full path to the system-wide SSH known_hosts file
known_hosts_file = "/etc/ssh/ssh_known_hosts"
# Location of the host SSH public key
host_public_key = "/etc/ssh/ssh_host_rsa_key.pub"
matchdata = host_public_key.match(/(^.*)(\.pub)$/)
# Location of the signed host SSH public key (will be created by this script)
signed_host_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
# Location of the CA's user public key (will be created by this script)
trusted_user_ca_key_file = "/etc/ssh/user_ca.pub"
auth_token = "foo"
# SSHD configurations files
sshd_config = "/etc/ssh/sshd_config"
sshd_config_backup = "/etc/ssh/sshd_config.keymgr.backup"
# Take our mode of operation from the first command line argument
mode = ARGV[0]
if ( mode == "-h" )
# This mode requires root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
# Obtain the hostname
if (ARGV[1])
fqdn=ARGV[1].chomp
else
puts "No fqdn supplied"
usage()
exit
end
matchdata = fqdn.match(/^([^\.]+)\.(.*)$/)
hostname = matchdata[1].chomp
dns_domain = matchdata[2].chomp
raw_public_key = File.read(host_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
# Submit REST call
response = RestClient.post "#{base_api_uri}#{host_sign_route}", \
:hostname => hostname, \
:dns_domain => dns_domain, \
:public_key => "#{encoded_public_key}", \
:auth_token => auth_token
# Parse the response (it should be in JSON format)
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{signed_host_key_file}."
File.open(signed_host_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_host_key']))}
if File.readlines(sshd_config).grep(/HostCertificate\s+#{signed_host_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("HostCertificate #{signed_host_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
# Request a user key be signed
elsif ( mode == "-u" )
# Set the default validity
validity="+24h"
username=ENV['USER'].chomp
# Obtain the username
if (ARGV[1])
principles=ARGV[1].chomp
end
# Obtain the location of the user's home directory from /etc/passwd
etc_passwd = (File.foreach('/etc/passwd').grep /^#{username}:/).to_s
home_dir_matchdata = etc_passwd.match(/^.*:([^:]+):[^:]+$/)
home_dir = home_dir_matchdata[1]
user_public_key = "#{home_dir}/.ssh/id_rsa.pub"
matchdata = user_public_key.match(/(^.*)(\.pub)$/)
signed_user_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
raw_public_key = File.read(user_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
if (principles)
principles_list = "#{username},#{principles}"
else
principles_list = username
end
# Submit REST call
response = RestClient.post "#{base_api_uri}#{user_sign_route}", \
:principles => principles_list, \
:public_key => encoded_public_key, \
:validity => validity, \
:auth_token => auth_token
# Parse the response, it should be in JSON format
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
signed_user_key = Base64.decode64(response_hash['encoded_signed_user_key'])
hash_signed_user_key = Digest::SHA256.hexdigest signed_user_key
hash_signed_user_key_file = 0
if (File.file?(signed_user_key_file))
hash_signed_user_key_file = (Digest::SHA256.file signed_user_key_file).hexdigest
end
if (hash_signed_user_key != hash_signed_user_key_file)
if (hash_signed_user_key_file == 0)
puts "Creating #{signed_user_key_file}"
else
puts "Overwriting #{signed_user_key_file}"
end
File.open(signed_user_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_user_key']))}
else
puts "No changes required for #{signed_user_key_file}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
# Update SSH daemon files
elsif (mode == "-d")
# Requres root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
response = RestClient.post "#{base_api_uri}#{daemon_update_route}", :auth_token => auth_token
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{known_hosts_file}."
File.open(known_hosts_file, 'w') { |file| file.write(Base64.decode64(response_hash['known_hosts']))}
trusted_user_ca_key = Base64.decode64(response_hash['trusted_user_ca_public_key'])
hash_trusted_user_ca_key = Digest::SHA256.hexdigest trusted_user_ca_key
# hash the user_ca file if it exists
hash_trusted_user_ca_key_file = 0
if (File.file?(trusted_user_ca_key_file))
hash_trusted_user_ca_key_file = (Digest::SHA256.file trusted_user_ca_key_file).hexdigest
end
# is the new user_ca key different from the one we already have
if (hash_trusted_user_ca_key != hash_trusted_user_ca_key_file)
if (hash_trusted_user_ca_key_file == 0)
puts "Creating #{trusted_user_ca_key_file}"
else
puts "Overwriting #{trusted_user_ca_key_file}"
end
File.open(trusted_user_ca_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['trusted_user_ca_public_key']))}
else
puts "No changes required for #{trusted_user_ca_key_file}"
end
if File.readlines(sshd_config).grep(/TrustedUserCAKeys\s+#{trusted_user_ca_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("TrustedUserCAKeys #{trusted_user_ca_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
end


#!/usr/bin/ruby
require 'rest-client'
require 'socket'
require 'base64'
require 'json'
require 'digest'
def usage()
puts
puts "Usage:"
puts " ruby keymgr.rb ( -u [userid2][,userid3]…| -d | -h fqdn )"
puts
puts " -u [userid2][,userid3]…"
puts " Request user public key signing for the current user (with other user principles too)"
puts " -d"
puts " Request that SSH daemon files (known_hosts and sshd_config) be updated (requires root)"
puts " -h fqdn"
puts " Requests host public key signing (requires root)"
puts
end
# Base part of our API's URI
base_api_uri = "http://vm-ca.fabrikam.com:4567/&quot;
# The list of routes supported by this API client
host_sign_route = "sign_host_key"
user_sign_route = "sign_user_key"
daemon_update_route = "daemon_update"
# Full path to the system-wide SSH known_hosts file
known_hosts_file = "/etc/ssh/ssh_known_hosts"
# Location of the host SSH public key
host_public_key = "/etc/ssh/ssh_host_rsa_key.pub"
matchdata = host_public_key.match(/(^.*)(\.pub)$/)
# Location of the signed host SSH public key (will be created by this script)
signed_host_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
# Location of the CA's user public key (will be created by this script)
trusted_user_ca_key_file = "/etc/ssh/user_ca.pub"
auth_token = "foo"
# SSHD configurations files
sshd_config = "/etc/ssh/sshd_config"
sshd_config_backup = "/etc/ssh/sshd_config.keymgr.backup"
# Take our mode of operation from the first command line argument
mode = ARGV[0]
if ( mode == "-h" )
# This mode requires root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
# Obtain the hostname
if (ARGV[1])
fqdn=ARGV[1].chomp
else
puts "No fqdn supplied"
usage()
exit
end
matchdata = fqdn.match(/^([^\.]+)\.(.*)$/)
hostname = matchdata[1].chomp
dns_domain = matchdata[2].chomp
raw_public_key = File.read(host_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
# Submit REST call
response = RestClient.post "#{base_api_uri}#{host_sign_route}", \
:hostname => hostname, \
:dns_domain => dns_domain, \
:public_key => "#{encoded_public_key}", \
:auth_token => auth_token
# Parse the response (it should be in JSON format)
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{signed_host_key_file}."
File.open(signed_host_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_host_key']))}
if File.readlines(sshd_config).grep(/HostCertificate\s+#{signed_host_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("HostCertificate #{signed_host_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
# Request a user key be signed
elsif ( mode == "-u" )
# Set the default validity
validity="+24h"
username=ENV['USER'].chomp
# Obtain the username
if (ARGV[1])
principles=ARGV[1].chomp
end
# Obtain the location of the user's home directory from /etc/passwd
etc_passwd = (File.foreach('/etc/passwd').grep /^#{username}:/).to_s
home_dir_matchdata = etc_passwd.match(/^.*:([^:]+):[^:]+$/)
home_dir = home_dir_matchdata[1]
user_public_key = "#{home_dir}/.ssh/id_rsa.pub"
matchdata = user_public_key.match(/(^.*)(\.pub)$/)
signed_user_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
raw_public_key = File.read(user_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
if (principles)
principles_list = "#{username},#{principles}"
else
principles_list = username
end
# Submit REST call
response = RestClient.post "#{base_api_uri}#{user_sign_route}", \
:principles => principles_list, \
:public_key => encoded_public_key, \
:validity => validity, \
:auth_token => auth_token
# Parse the response, it should be in JSON format
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
signed_user_key = Base64.decode64(response_hash['encoded_signed_user_key'])
hash_signed_user_key = Digest::SHA256.hexdigest signed_user_key
hash_signed_user_key_file = 0
if (File.file?(signed_user_key_file))
hash_signed_user_key_file = (Digest::SHA256.file signed_user_key_file).hexdigest
end
if (hash_signed_user_key != hash_signed_user_key_file)
if (hash_signed_user_key_file == 0)
puts "Creating #{signed_user_key_file}"
else
puts "Overwriting #{signed_user_key_file}"
end
File.open(signed_user_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_user_key']))}
else
puts "No changes required for #{signed_user_key_file}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
# Update SSH daemon files
elsif (mode == "-d")
# Requres root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
response = RestClient.post "#{base_api_uri}#{daemon_update_route}", :auth_token => auth_token
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{known_hosts_file}."
File.open(known_hosts_file, 'w') { |file| file.write(Base64.decode64(response_hash['known_hosts']))}
trusted_user_ca_key = Base64.decode64(response_hash['trusted_user_ca_public_key'])
hash_trusted_user_ca_key = Digest::SHA256.hexdigest trusted_user_ca_key
# hash the user_ca file if it exists
hash_trusted_user_ca_key_file = 0
if (File.file?(trusted_user_ca_key_file))
hash_trusted_user_ca_key_file = (Digest::SHA256.file trusted_user_ca_key_file).hexdigest
end
# is the new user_ca key different from the one we already have
if (hash_trusted_user_ca_key != hash_trusted_user_ca_key_file)
if (hash_trusted_user_ca_key_file == 0)
puts "Creating #{trusted_user_ca_key_file}"
else
puts "Overwriting #{trusted_user_ca_key_file}"
end
File.open(trusted_user_ca_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['trusted_user_ca_public_key']))}
else
puts "No changes required for #{trusted_user_ca_key_file}"
end
if File.readlines(sshd_config).grep(/TrustedUserCAKeys\s+#{trusted_user_ca_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("TrustedUserCAKeys #{trusted_user_ca_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
end


#!/usr/bin/ruby
require 'rest-client'
require 'socket'
require 'base64'
require 'json'
require 'digest'
def usage()
puts
puts "Usage:"
puts " ruby keymgr.rb ( -u [userid2][,userid3]…| -d | -h fqdn )"
puts
puts " -u [userid2][,userid3]…"
puts " Request user public key signing for the current user (with other user principles too)"
puts " -d"
puts " Request that SSH daemon files (known_hosts and sshd_config) be updated (requires root)"
puts " -h fqdn"
puts " Requests host public key signing (requires root)"
puts
end
# Base part of our API's URI
base_api_uri = "http://vm-ca.fabrikam.com:4567/&quot;
# The list of routes supported by this API client
host_sign_route = "sign_host_key"
user_sign_route = "sign_user_key"
daemon_update_route = "daemon_update"
# Full path to the system-wide SSH known_hosts file
known_hosts_file = "/etc/ssh/ssh_known_hosts"
# Location of the host SSH public key
host_public_key = "/etc/ssh/ssh_host_rsa_key.pub"
matchdata = host_public_key.match(/(^.*)(\.pub)$/)
# Location of the signed host SSH public key (will be created by this script)
signed_host_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
# Location of the CA's user public key (will be created by this script)
trusted_user_ca_key_file = "/etc/ssh/user_ca.pub"
auth_token = "foo"
# SSHD configurations files
sshd_config = "/etc/ssh/sshd_config"
sshd_config_backup = "/etc/ssh/sshd_config.keymgr.backup"
# Take our mode of operation from the first command line argument
mode = ARGV[0]
if ( mode == "-h" )
# This mode requires root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
# Obtain the hostname
if (ARGV[1])
fqdn=ARGV[1].chomp
else
puts "No fqdn supplied"
usage()
exit
end
matchdata = fqdn.match(/^([^\.]+)\.(.*)$/)
hostname = matchdata[1].chomp
dns_domain = matchdata[2].chomp
raw_public_key = File.read(host_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
# Submit REST call
response = RestClient.post "#{base_api_uri}#{host_sign_route}", \
:hostname => hostname, \
:dns_domain => dns_domain, \
:public_key => "#{encoded_public_key}", \
:auth_token => auth_token
# Parse the response (it should be in JSON format)
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{signed_host_key_file}."
File.open(signed_host_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_host_key']))}
if File.readlines(sshd_config).grep(/HostCertificate\s+#{signed_host_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("HostCertificate #{signed_host_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
# Request a user key be signed
elsif ( mode == "-u" )
# Set the default validity
validity="+24h"
username=ENV['USER'].chomp
# Obtain the username
if (ARGV[1])
principles=ARGV[1].chomp
end
# Obtain the location of the user's home directory from /etc/passwd
etc_passwd = (File.foreach('/etc/passwd').grep /^#{username}:/).to_s
home_dir_matchdata = etc_passwd.match(/^.*:([^:]+):[^:]+$/)
home_dir = home_dir_matchdata[1]
user_public_key = "#{home_dir}/.ssh/id_rsa.pub"
matchdata = user_public_key.match(/(^.*)(\.pub)$/)
signed_user_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
raw_public_key = File.read(user_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
if (principles)
principles_list = "#{username},#{principles}"
else
principles_list = username
end
# Submit REST call
response = RestClient.post "#{base_api_uri}#{user_sign_route}", \
:principles => principles_list, \
:public_key => encoded_public_key, \
:validity => validity, \
:auth_token => auth_token
# Parse the response, it should be in JSON format
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
signed_user_key = Base64.decode64(response_hash['encoded_signed_user_key'])
hash_signed_user_key = Digest::SHA256.hexdigest signed_user_key
hash_signed_user_key_file = 0
if (File.file?(signed_user_key_file))
hash_signed_user_key_file = (Digest::SHA256.file signed_user_key_file).hexdigest
end
if (hash_signed_user_key != hash_signed_user_key_file)
if (hash_signed_user_key_file == 0)
puts "Creating #{signed_user_key_file}"
else
puts "Overwriting #{signed_user_key_file}"
end
File.open(signed_user_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_user_key']))}
else
puts "No changes required for #{signed_user_key_file}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
# Update SSH daemon files
elsif (mode == "-d")
# Requres root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
response = RestClient.post "#{base_api_uri}#{daemon_update_route}", :auth_token => auth_token
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{known_hosts_file}."
File.open(known_hosts_file, 'w') { |file| file.write(Base64.decode64(response_hash['known_hosts']))}
trusted_user_ca_key = Base64.decode64(response_hash['trusted_user_ca_public_key'])
hash_trusted_user_ca_key = Digest::SHA256.hexdigest trusted_user_ca_key
# hash the user_ca file if it exists
hash_trusted_user_ca_key_file = 0
if (File.file?(trusted_user_ca_key_file))
hash_trusted_user_ca_key_file = (Digest::SHA256.file trusted_user_ca_key_file).hexdigest
end
# is the new user_ca key different from the one we already have
if (hash_trusted_user_ca_key != hash_trusted_user_ca_key_file)
if (hash_trusted_user_ca_key_file == 0)
puts "Creating #{trusted_user_ca_key_file}"
else
puts "Overwriting #{trusted_user_ca_key_file}"
end
File.open(trusted_user_ca_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['trusted_user_ca_public_key']))}
else
puts "No changes required for #{trusted_user_ca_key_file}"
end
if File.readlines(sshd_config).grep(/TrustedUserCAKeys\s+#{trusted_user_ca_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("TrustedUserCAKeys #{trusted_user_ca_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
end

 

 

 

Leave a comment