-
-
Save UmbrielSecurity/8b37c30c7e1ff89f82e6c8b93ef3881e to your computer and use it in GitHub Desktop.
Ruby client to request user/host SSH key signing (see: ssh_ca_api.rb)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment