init
This commit is contained in:
commit
cb2306889f
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# ---> Vim
|
||||||
|
# Swap
|
||||||
|
[._]*.s[a-v][a-z]
|
||||||
|
!*.svg # comment out if you don't need vector files
|
||||||
|
[._]*.sw[a-p]
|
||||||
|
[._]s[a-rt-v][a-z]
|
||||||
|
[._]ss[a-gi-z]
|
||||||
|
[._]sw[a-p]
|
||||||
|
|
||||||
|
# Session
|
||||||
|
Session.vim
|
||||||
|
Sessionx.vim
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
.netrwhist
|
||||||
|
*~
|
||||||
|
# Auto-generated tag files
|
||||||
|
tags
|
||||||
|
# Persistent undo
|
||||||
|
[._]*.un~
|
||||||
|
|
||||||
|
# ---> Ansible
|
||||||
|
*.retry
|
||||||
|
|
61
README.adoc
Normal file
61
README.adoc
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
|
A brief description of the role goes here.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
|
|
||||||
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
|
ssh_ca_name::
|
||||||
|
Name for your CA - will be stored as comment.
|
||||||
|
If CA already exists, it will not be changed.
|
||||||
|
Mandatory.
|
||||||
|
|
||||||
|
ssh_ca_user::
|
||||||
|
User for CA.
|
||||||
|
Must match with `ssh_cert`-role.
|
||||||
|
Default: `sshca`
|
||||||
|
|
||||||
|
ssh_ca_home::
|
||||||
|
Default: `/var/lib/sshca`
|
||||||
|
|
||||||
|
ssh_ca_base_dir::
|
||||||
|
Where to store the certs and CA.
|
||||||
|
Must match with `ssh_cert`-role.
|
||||||
|
Default: `~/.ssh-ca`
|
||||||
|
**Do not change!**
|
||||||
|
|
||||||
|
ssh_ca_force_regeneration::
|
||||||
|
Forces to regenerate the CA.
|
||||||
|
*The old will be deleted!**
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
Use ssh-cert to use ssh-ca-server for re-/newal hosts and users certificates.
|
||||||
|
|
||||||
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.example playbook
|
||||||
|
----
|
||||||
|
- name: SSH-CA
|
||||||
|
hosts: ssh_ca_server
|
||||||
|
roles:
|
||||||
|
- role: ssh-ca
|
||||||
|
----
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
AGPLv3
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Denis Knauf - https://git.denkn.at/deac/ansible-role-ssh-cert
|
2
defaults/main.yml
Normal file
2
defaults/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
# defaults file for ssh-ca
|
328
files/ssh-ca.rb
Executable file
328
files/ssh-ca.rb
Executable file
|
@ -0,0 +1,328 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require 'time'
|
||||||
|
require 'pathname'
|
||||||
|
require 'shellwords'
|
||||||
|
require 'getoptlong'
|
||||||
|
require 'active_support/time'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
def help msg=nil
|
||||||
|
STDERR.puts msg if msg
|
||||||
|
STDERR.puts <<-EOHELP.gsub( /^\s*:/, '')
|
||||||
|
:Usage: ssh .... renew [force] [show]
|
||||||
|
: ssh .... show
|
||||||
|
: ssh .... help
|
||||||
|
: ssh .... jsonl
|
||||||
|
:
|
||||||
|
:[h]elp
|
||||||
|
:[r]enew Renews (if expires in graceperiod) or creates (if not existing) certificate
|
||||||
|
: [f]orce forces renewal, also if valid for graceperiod
|
||||||
|
:[s]how Shows current certificate
|
||||||
|
:jsonl Expect commands via STDIN as JSONL and responses via STDOUT in JSONL
|
||||||
|
: Request format: [0, CMD, ARGS...]
|
||||||
|
: Response format: [SCODE, BODY]
|
||||||
|
: SCODEs are like HTTP.
|
||||||
|
:
|
||||||
|
:possible CMD:
|
||||||
|
:<renew[, {<force: true|false>}]> => <RENEWED, DATA, CONTENT>
|
||||||
|
:<get> => <DATA, CONTENT>
|
||||||
|
:
|
||||||
|
: RENEWED indicates if file was renewed (true|false).
|
||||||
|
: CONTENT is the content of the certfile (String).
|
||||||
|
: DATA is the parsed file (ssh-keygen -L) (Hash).
|
||||||
|
EOHELP
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
class Die <Exception
|
||||||
|
end
|
||||||
|
|
||||||
|
module Kernel
|
||||||
|
def popen *a, **o, &e
|
||||||
|
a = a.flatten.compact.map &:to_s
|
||||||
|
IO.popen a, **o, &e
|
||||||
|
end
|
||||||
|
|
||||||
|
def die str
|
||||||
|
raise Die, str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module FileHelper
|
||||||
|
attr_reader :file
|
||||||
|
|
||||||
|
def exist?
|
||||||
|
@file.file?
|
||||||
|
end
|
||||||
|
|
||||||
|
def read
|
||||||
|
@file.read
|
||||||
|
end
|
||||||
|
|
||||||
|
def open *a, **o, &e
|
||||||
|
if block_given?
|
||||||
|
@file.open *a, **o, &e
|
||||||
|
else
|
||||||
|
@file.open *a, **o
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
@file.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PublicKeyFile
|
||||||
|
include FileHelper
|
||||||
|
attr_reader :type, :ident
|
||||||
|
|
||||||
|
def initialize file, type, ident
|
||||||
|
@file, @type, @ident = file, type, ident
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare authkeysfile
|
||||||
|
return true if exist?
|
||||||
|
authkeysfile.each_line do |l|
|
||||||
|
case l.chomp
|
||||||
|
when /\A\s*#/ # ignore - comment
|
||||||
|
when /\A\s*$/ # ignore - empty line
|
||||||
|
when /\Assh-/ # ignore - blank keys without command
|
||||||
|
when / (ssh-(?:rsa|ed25519) +[^ ]+ +#{Regexp.quote @ident}) *\z/
|
||||||
|
pub = $1
|
||||||
|
@file.open( 'a+') {|f| f.puts pub }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
exist?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class CertifcateFile
|
||||||
|
include FileHelper
|
||||||
|
attr_reader :metadata
|
||||||
|
|
||||||
|
def initialize file
|
||||||
|
@file = file
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse
|
||||||
|
r = {}
|
||||||
|
popen %w[ssh-keygen -L -f], @file do |f|
|
||||||
|
mode = nil
|
||||||
|
f.each_line do |l|
|
||||||
|
case l.chomp
|
||||||
|
when /^ Valid: from ([^ ][^ ]*) to ([^ ][^ ]*)$/
|
||||||
|
r[:valid] = Time.parse( $1) ... Time.parse( $2)
|
||||||
|
when /^ Type: ([^ ]+) ([^ ]+) certificate$/
|
||||||
|
r[:type] = [$1, $2]
|
||||||
|
when /^ Public key: ([^ ]+) (.*)$/
|
||||||
|
r[:pubkey]= [$1, $2]
|
||||||
|
when /^ Signing CA: ([^ ]+) (.*)$/
|
||||||
|
r[:signca]= [$1, $2]
|
||||||
|
when /^ Key ID: (.*)$/
|
||||||
|
r[:keyid] = $1.sub /\A"?(.*?)"?\z/, '\1'
|
||||||
|
when /^ Serial: (\d+)$/
|
||||||
|
r[:serial]= $1.to_i
|
||||||
|
when /^ Principals: \(node\)$/
|
||||||
|
r[:principals] = []
|
||||||
|
when /^ Principals:\s*$/
|
||||||
|
mode = :principals
|
||||||
|
r[:principals] = []
|
||||||
|
when /^ Extensions: \(none\)$/
|
||||||
|
r[:extensions] = []
|
||||||
|
when /^ Extensions:\s*$/
|
||||||
|
mode = :extensions
|
||||||
|
r[:extensions] = []
|
||||||
|
when /^ Critical Options: \(none\)$/
|
||||||
|
r[:critopts] = []
|
||||||
|
when /^ Critical Options:\s*$/
|
||||||
|
mode = :critopts
|
||||||
|
r[:critopts] = []
|
||||||
|
when /^ ([^ ].*)$/
|
||||||
|
r[mode].push $1
|
||||||
|
else
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@metadata = r
|
||||||
|
end
|
||||||
|
|
||||||
|
def [] key
|
||||||
|
(@metadata || parse)[key]
|
||||||
|
end
|
||||||
|
alias to_hash []
|
||||||
|
|
||||||
|
def valid_at? ts
|
||||||
|
exist? and self[:valid].include?( ts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create pubfile, type:, valid:, serial:, cafile:, info:, principals:, sshcadir:
|
||||||
|
#sign_user_pub opts
|
||||||
|
err =
|
||||||
|
popen( %w[ssh-keygen -V], "#{valid.begin.strftime '%Y%m%d'}:#{valid.end.strftime '%Y%m%d'}",
|
||||||
|
(:host == type ? '-h' : nil), '-s', cafile,
|
||||||
|
'-P', '', '-z', serial, '-I', info, '-n', principals.join(','), pubfile,
|
||||||
|
err: %i[child out]
|
||||||
|
) {|l| l.read }
|
||||||
|
die "Creating certificate failed [#{$?.inspect}]: #{err}" unless 0 == $?.exitstatus
|
||||||
|
serialkeyfile = sshcadir + 'serials' + serial.to_s
|
||||||
|
serialkeyfile.open( 'a+') {|f| f.write self.read }
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class SerialFile
|
||||||
|
include FileHelper
|
||||||
|
|
||||||
|
def initialize file
|
||||||
|
@file = file
|
||||||
|
end
|
||||||
|
|
||||||
|
def read
|
||||||
|
open 'a+' do |fh|
|
||||||
|
fh.read.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias to_i read
|
||||||
|
alias get read
|
||||||
|
|
||||||
|
def increment!
|
||||||
|
open 'a+' do |fh|
|
||||||
|
(1+fh.read.to_i).tap do |val|
|
||||||
|
fh.rewind
|
||||||
|
fh.truncate 0
|
||||||
|
fh.print val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias inc! increment!
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
begin
|
||||||
|
|
||||||
|
die "2..4 arguments (type ident [principals [info]]) expected. Provided count of arguments: #{ARGV.length}" unless (2..4) === ARGV.length
|
||||||
|
type, ident, principals, info = ARGV[0..3]
|
||||||
|
expire_in, gracetime = 12.weeks, 1.week
|
||||||
|
type = type.to_sym
|
||||||
|
case type
|
||||||
|
when :host
|
||||||
|
die "Invalid ident: #{ident}" unless ident =~ /\A[a-z][a-z_0-9]*\z/i
|
||||||
|
expire_in, gracetime = 360.days, 30.days
|
||||||
|
info ||= "host: #{ident}"
|
||||||
|
principals =
|
||||||
|
([ident] +
|
||||||
|
principals.split( ',').map do |e|
|
||||||
|
if /:/ =~ e or /\A[0-9.]+\z/ =~ e or /\A[^.]+\z/ =~ e
|
||||||
|
e
|
||||||
|
else
|
||||||
|
e = e.gsub /\.?\z/, ''
|
||||||
|
[e, "#{e}."]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
).flatten.compact.map do |e|
|
||||||
|
if /\A\[/ =~ e
|
||||||
|
e
|
||||||
|
else
|
||||||
|
[e, "[#{e}]:19"]
|
||||||
|
end
|
||||||
|
end.flatten.compact.uniq
|
||||||
|
when :user
|
||||||
|
die "Invalid ident: #{ident}" unless ident =~ /\A[a-z][a-z_0-9]*@[a-z][a-z_0-9]*\z/i
|
||||||
|
info ||= "user: #{ident}"
|
||||||
|
principals = principals.split ','
|
||||||
|
else
|
||||||
|
die "Invalid type: #{type}"
|
||||||
|
end
|
||||||
|
|
||||||
|
sshcadir = Pathname.new( "~/.ssh-ca").expand_path
|
||||||
|
pubfile = PublicKeyFile.new sshcadir + "#{type}-#{ident}.pub", type, ident
|
||||||
|
certfile = CertifcateFile.new sshcadir + "#{type}-#{ident}-cert.pub"
|
||||||
|
cafile = sshcadir + "ca"
|
||||||
|
capubfile = sshcadir + "ca.pub"
|
||||||
|
serialfile = SerialFile.new sshcadir + "serial"
|
||||||
|
authkeysfile = Pathname.new( "~/.ssh/authorized_keys").expand_path
|
||||||
|
|
||||||
|
help unless ENV['SSH_ORIGINAL_COMMAND']
|
||||||
|
die "Unallowed char in arguments [\\0]" if ENV['SSH_ORIGINAL_COMMAND'].include? 0.chr
|
||||||
|
ARGV.replace ENV['SSH_ORIGINAL_COMMAND'].shellsplit
|
||||||
|
case ARGV[0]
|
||||||
|
when *%w[h help]
|
||||||
|
help
|
||||||
|
|
||||||
|
when *%w[s show]
|
||||||
|
die "Unexpected addition parameters: #{ARGV.shelljoin}" unless 1 == ARGV.length
|
||||||
|
die "No certfile, yet: #{certfile.basename}" unless certfile.exist?
|
||||||
|
puts certfile.read
|
||||||
|
|
||||||
|
when *%w[r renew]
|
||||||
|
begin
|
||||||
|
unexpected = ARGV[1..-1] - %w[s show f force]
|
||||||
|
die "Unknown argument: #{unexpected.shelljoin}" unless unexpected.empty?
|
||||||
|
end
|
||||||
|
show = ! (ARGV[1..-1] & %w[s show]).empty?
|
||||||
|
force = ! (ARGV[1..-1] & %w[f force]).empty?
|
||||||
|
|
||||||
|
pubfile.prepare authkeysfile
|
||||||
|
die "Pubfile or pub in authorized_keys for #{ident} not found." unless pubfile.exist?
|
||||||
|
|
||||||
|
if !force && certfile.exist? && certfile.valid_at?( gracetime.from_now) && certfile[:principals].sort == principals.sort
|
||||||
|
STDERR.print "Certificate [#{certfile[:serial]}|#{certfile[:keyid]}] valid between #{certfile[:valid]} as #{certfile[:principals].sort.join ', '}\n"
|
||||||
|
STDERR.flush
|
||||||
|
STDOUT.print "#{certfile.read.chomp}\n" if show
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
|
|
||||||
|
STDERR.puts "Renew certificate for #{ident} as #{principals.sort.join ', '}"
|
||||||
|
certfile.create pubfile, type: type, valid: -5.minutes.ago..expire_in.from_now,
|
||||||
|
serial: serialfile.increment!, cafile: cafile, info: info, principals: principals, sshcadir: sshcadir
|
||||||
|
puts certfile.read if show
|
||||||
|
|
||||||
|
when 'jsonl'
|
||||||
|
STDIN.each_line do |line|
|
||||||
|
begin
|
||||||
|
line = JSON.parse line, symbolize_names: true
|
||||||
|
cmd, args = line[0].to_sym, line[1..-1]
|
||||||
|
case cmd
|
||||||
|
when :renew
|
||||||
|
die "renew expects no or a Hash as args." unless (0..1).include?( args.length) && args.kind_of?( Hash)
|
||||||
|
args ||= {}
|
||||||
|
args[:force] = ! ! args[:force]
|
||||||
|
|
||||||
|
pubfile.prepare
|
||||||
|
die "Pubfile or pub in authorized_keys for #{ident} not found." unless pubfile.exist?
|
||||||
|
|
||||||
|
if !args[:force] && certfile.valid_at?( gracetime.from_now)
|
||||||
|
STDOUT.puts [200, false, certfile.metadata, certfile.read].to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
certfile.create pubfile, type: type, sshcadir: sshcadir,
|
||||||
|
valid: -5.minutes.ago..expire_in.from_now, serial: serialfile.increment!,
|
||||||
|
cafile: cafile, info: info, principals: principals
|
||||||
|
STDOUT.puts [200, true, certfile.metadata, certfile.read].to_json
|
||||||
|
|
||||||
|
when :get
|
||||||
|
STDOUT.puts [200, certfile.metadata, certfile.read].to_json
|
||||||
|
|
||||||
|
else
|
||||||
|
die "Unknown command."
|
||||||
|
end
|
||||||
|
rescue Object
|
||||||
|
STDOUT.puts [500, $!.class.name, $!.to_s].to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
help "Unknown command: #{ARGV[0]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue SystemExit
|
||||||
|
raise
|
||||||
|
rescue Die
|
||||||
|
STDERR.puts $!
|
||||||
|
exit 1
|
||||||
|
rescue Object
|
||||||
|
STDERR.puts "#$! (#{$!.class})", *$!.backtrace
|
||||||
|
exit 2
|
||||||
|
end
|
2
handlers/main.yml
Normal file
2
handlers/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
# handlers file for ssh-ca
|
32
meta/main.yml
Normal file
32
meta/main.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
galaxy_info:
|
||||||
|
author: Denis Knauf
|
||||||
|
description: Provides a SSH-CA renewal server
|
||||||
|
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
license: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
min_ansible_version: 2.9
|
||||||
|
|
||||||
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
|
# min_ansible_container_version:
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
- name: Debian
|
||||||
|
versions:
|
||||||
|
- 10
|
||||||
|
- name: Ubuntu
|
||||||
|
versions:
|
||||||
|
- 18.04
|
||||||
|
- 20.04
|
||||||
|
|
||||||
|
galaxy_tags: []
|
||||||
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
27
tasks/main.yml
Normal file
27
tasks/main.yml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
# vim: set expandtab tabstop=2 shiftwidth=2:
|
||||||
|
- name: create sshca-user
|
||||||
|
user:
|
||||||
|
name: '{{ssh_ca_user}}'
|
||||||
|
comment: SSH-CA
|
||||||
|
shell: /bin/sh
|
||||||
|
createhome: yes
|
||||||
|
home: '{{ssh_ca_home}}'
|
||||||
|
move_home: no
|
||||||
|
skeleton: no
|
||||||
|
- name: install ssh-ca
|
||||||
|
copy:
|
||||||
|
src: ssh-ca.rb
|
||||||
|
dest: '{{ssh_ca_home}}/ssh-ca'
|
||||||
|
- name: base-dir
|
||||||
|
file:
|
||||||
|
path: '{{ssh_ca_base_dir}}'
|
||||||
|
owner: '{{ssh_ca_user}}'
|
||||||
|
mode: 0700
|
||||||
|
- name: CA
|
||||||
|
openssh_keypair:
|
||||||
|
path: '{{ssh_ca_base_dir}}/ca'
|
||||||
|
type: ed25519
|
||||||
|
owner: '{{ssh_ca_user}}'
|
||||||
|
comment: '{{ssh_ca_name|mandatory}}'
|
||||||
|
force: '{{ssh_ca_force_regeneration|default(false)}}'
|
2
tests/inventory
Normal file
2
tests/inventory
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
localhost
|
||||||
|
|
6
tests/test.yml
Normal file
6
tests/test.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
# vim: set et sw=2 ts=2 sts=2:
|
||||||
|
- hosts: localhost
|
||||||
|
remote_user: root
|
||||||
|
roles:
|
||||||
|
- ssh-ca
|
5
vars/main.yml
Normal file
5
vars/main.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
# vim: set et sw=2 ts=2 sws=2:
|
||||||
|
ssh_ca_user: sshca
|
||||||
|
ssh_ca_home: /var/lib/sshca
|
||||||
|
ssh_ca_base_dir: ~/.ssh-ca
|
Loading…
Reference in a new issue