Browse Source

roles/ca_cert: new role!

New role to automate generation and issuing of certificate using a
`ca_manager` server.

Code is more sintentic and concise, and we avoid duplications.
python3
Zolfa 4 years ago
parent
commit
c03b9af325
Signed by: zolfa GPG Key ID: E1A43B038C4D6616
10 changed files with 283 additions and 243 deletions
  1. +54
    -18
      library/ssh_cert.py
  2. +15
    -0
      roles/ca_cert/defaults/main.yaml
  3. +156
    -0
      roles/ca_cert/tasks/main.yaml
  4. +2
    -2
      roles/ldap/defaults/main.yaml
  5. +19
    -46
      roles/ldap/tasks/4_setup_tls.yaml
  6. +9
    -42
      roles/openvpn/tasks/main.yaml
  7. +26
    -80
      roles/ssh_server/tasks/main.yaml
  8. +0
    -4
      roles/ssh_server/templates/user_ca.pub.j2
  9. +2
    -9
      tasks/ca-dialog.yaml
  10. +0
    -42
      tasks/ca-signing-request.yaml

+ 54
- 18
library/ssh_cert.py View File

@ -4,7 +4,7 @@ from datetime import datetime
import string
import subprocess
from ansible.module_utils.basic import *
from ansible.module_utils.basic import AnsibleModule
__doc__ = '''
module: ssh_cert
@ -29,20 +29,30 @@ def signin_ca(lines):
# has changed, this should work for all versions:
return l.split()[3]
def principals(lines):
principals = []
reading = False
for l in lines:
if l.startswith('Critical Options:'):
reading = False
if reading:
principals.append(l)
if l == 'Principals:':
reading = True
return principals
def still_valid(cert_timestamps):
t = datetime.datetime.today()
t = datetime.today()
return t < cert_timestamps['valid']['to'] and t > cert_timestamps['valid']['from']
def expired(cert_timestamps):
t = datetime.datetime.today()
t = datetime.today()
return t > cert_timestamps['valid']['to']
def not_valid(cert_timestamps):
t = datetime.datetime.today()
t = datetime.today()
return t < cert_timestamps['valid']['from']
@ -55,27 +65,43 @@ def cert_type(lines):
def valid_from(lines):
for l in lines:
if l.startswith('Valid'):
return datetime.datetime.strptime(l.split()[2], CERT_TIME_FORMAT)
return datetime.strptime(l.split()[2], CERT_TIME_FORMAT)
def valid_to(lines):
for l in lines:
if l.startswith('Valid'):
return datetime.datetime.strptime(l.split()[4], CERT_TIME_FORMAT)
return datetime.strptime(l.split()[4], CERT_TIME_FORMAT)
def main():
module = AnsibleModule(
argument_spec=dict(),
supports_check_mode=False,
)
argument_spec=dict(
principals=dict(
required=True,
type='list',
),
path=dict(
required=False,
type='str',
default='/etc/ssh/ssh_host_ed25519_key-cert.pub',
),
ca_path=dict(
required=False,
type='str',
default='/etc/ssh/user_ca.pub',
),
),
supports_check_mode=False,
)
result = {}
result['rc'] = 0
result['msg'] = ''
result['failed'] = False
result['ca'] = {}
result['ca']['path'] = '/etc/ssh/user_ca.pub'
result['ca']['path'] = module.params.get('ca_path')
result['certificate'] = {}
result['certificate']['path'] = '/etc/ssh/ssh_host_ed25519_key-cert.pub'
result['certificate']['path'] = module.params.get('path')
ca_output = subprocess.check_output([
'ssh-keygen',
@ -97,27 +123,37 @@ def main():
cert_lines = [line.strip() for line in cert_output.decode().split('\n')]
result['certificate']['signin_ca'] = signin_ca(cert_lines)
result['certificate']['principals'] = principals(cert_lines)
result['certificate']['valid'] = {
'from': valid_from(cert_lines),
'to': valid_to(cert_lines),
'remaining_days': (valid_to(cert_lines)-datetime.now()).days
}
if not still_valid(result['certificate']):
result['failed'] = True
result['msg'] = 'The certificate is not valid now'
result['msg'] += 'The certificate is not valid now. '
if not_valid(result['certificate']):
result['rc'] = 2
result['rc'] += 2
if expired(result['certificate']):
result['rc'] = 3
result['rc'] += 4
result['certificate']['serial'] = serial(cert_lines)
result['certificate']['type'] = cert_type(cert_lines)
if not result['certificate']['signin_ca'] == result['ca']['fingerprint']:
result['failed'] = True
result['msg'] = 'The provided CA did not sign the certificate specified'
result['rc'] = 1
result['msg'] = 'The provided CA did not sign the certificate specified. '
result['rc'] += 1
principal_mismatch = False
for principal in module.params.get('principals'):
if not principal in result['certificate']['principals']:
principal_mismatch = True
result['msg'] += 'Principal {} not found in cert. '.format(principal)
if principal_mismatch:
result['failed'] = True
result['rc'] += 8
module.exit_json(**result)


+ 15
- 0
roles/ca_cert/defaults/main.yaml View File

@ -0,0 +1,15 @@
---
ca_cert_ca_manager_host: 'authorities_request'
ca_cert_common_name: '{{ host_fqdn }}'
ca_cert_proto: 'tls'
ca_cert_client: false
ca_cert_min_days_validity: 30
ca_cert_renew_private_key: true
ca_cert_tls_subj: '{{ openssl_x509_prefix}}/OU=Server/CN={{ ca_cert_common_name }}'
ca_cert_tls_ca_path: '/etc/ssl/root_ca.crt'
ca_cert_tls_key_path: '/etc/ssl/{{ ca_cert_common_name }}.key'
ca_cert_tls_csr_path: '/etc/ssl/{{ ca_cert_common_name }}.csr'
ca_cert_tls_cert_path: '/etc/ssl/{{ ca_cert_common_name }}.crt'
ca_cert_ssh_ca_path: '/etc/ssh/user_ca.pub'
ca_cert_ssh_key_path: '/etc/ssh/ssh_host_ed25519_key'
...

+ 156
- 0
roles/ca_cert/tasks/main.yaml View File

@ -0,0 +1,156 @@
---
- name: 'TLS | verify if cert is valid'
command: >
openssl verify
-CAfile {{ ca_cert_tls_ca_path }}
-verify_hostname {{ ca_cert_common_name }}
{{ ca_cert_tls_cert_path }}
register: ca_cert_tls_cert_is_valid
check_mode: false
changed_when: ca_cert_tls_cert_is_valid.rc != 0
failed_when: false
when: ca_cert_proto == 'tls'
- name: 'SSH | verify if cert is valid and get info'
ssh_cert:
path: '{{ ca_cert_ssh_key_path }}-cert.pub'
ca_path: '{{ ca_cert_ssh_ca_path }}'
principals: [ '{{ ca_cert_common_name }}' ]
register: ca_cert_ssh_cert_is_valid
changed_when: ca_cert_ssh_cert_is_valid.rc != 0
ignore_errors: true
check_mode: false
when: ca_cert_proto == 'ssh'
- name: 'TLS | get remaining validity'
shell: >
{% if ansible_distribution != 'OpenWrt' %}
echo $(( ($(date -d "$(openssl x509 -in {{ ca_cert_tls_cert_path }} -enddate -noout | sed "s/.*=\(.*\)/\1/")" +%s)-$(date -d now +%s))/86400 ))
{% else %}
echo $(( ($(date -D '%b %e %H:%M:%S %Y' -d "$(openssl x509 -in {{ ca_cert_tls_cert_path }} -enddate -noout | sed "s/.*=\(.*\)/\1/")" +%s)-$(date +%s))/86400 ))
{% endif %}
register: ca_cert_cert_remaining_days
changed_when: false
check_mode: false
when: ca_cert_proto == 'tls' and not ca_cert_tls_cert_is_valid.changed
- name: 'set cert validity'
set_fact:
ca_cert_cert_is_valid: >-
{% if ca_cert_proto == 'tls' %}{{ ca_cert_tls_cert_is_valid }}{%
elif ca_cert_proto == 'ssh' %}{{ ca_cert_ssh_cert_is_valid }}{% endif %}
- name: 'set remaning validity'
set_fact:
ca_cert_cert_remaining_days: >-
{% if ca_cert_proto == 'tls' %}{{ ca_cert_cert_remaining_days.stdout }}{%
elif ca_cert_proto == 'ssh' %}{{ ca_cert_cert_is_valid.certificate.valid.remaining_days }}{% endif %}
when: ca_cert_cert_is_valid.rc|d(1) == 0
- name: 'renew'
block:
- name: 'RENEW | backup existing private keys'
copy:
remote_src: true
src: '{{ item }}'
dest: '{{ item }}-backup'
failed_when: false
register: ca_cert_key_backup
loop: '{{ keypair[ca_cert_proto] }}'
vars:
keypair:
ssh:
- '{{ ca_cert_ssh_key_path }}'
- '{{ ca_cert_ssh_key_path }}.pub'
tls:
- '{{ ca_cert_tls_key_path }}'
- name: 'RENEW | TLS | create private key (if not exists)'
command: >
openssl genpkey
-algorithm ed25519
-out {{ ca_cert_tls_key_path }}
args:
creates: >-
{{ "" if ca_cert_renew_private_key else ca_cert_tls_key_path }}
when: ca_cert_proto == 'tls'
- name: 'RENEW | SSH | create key pair'
openssh_keypair:
force: '{{ ca_cert_renew_private_key }}'
path: '{{ ca_cert_ssh_key_path }}'
type: 'ed25519'
when: ca_cert_proto == 'ssh'
- name: 'RENEW | TLS | create cert signing request'
command: >
openssl req
-new
-subj '{{ ca_cert_tls_subj }}'
-key '{{ ca_cert_tls_key_path }}'
-out '{{ ca_cert_tls_csr_path }}'
when: ca_cert_proto == 'tls'
- name: 'RENEW | CA_MANAGER | generate json signing request'
cert_request:
host: '{{ ca_cert_common_name }}'
path: >-
{% if ca_cert_proto == 'tls' %}{{ ca_cert_tls_csr_path }}{%
elif ca_cert_proto == 'ssh' %}{{ ca_cert_ssh_key_path+'.pub' }}{% endif %}
proto: '{{ "ssl" if ca_cert_proto == "tls" else ca_cert_proto }}'
client: '{{ ca_cert_client }}'
register: ca_cert_signing_request
- name: 'RENEW | CA_MANAGER | send signing request'
raw: '{{ ca_cert_signing_request | to_json }}'
delegate_to: '{{ ca_cert_ca_manager_host }}'
delegate_facts: true
register: ca_cert_signing_request_results
failed_when: (ca_cert_signing_request_results.stdout|from_json).failed
- name: 'RENEW | CA_MANAGER | set signing request id'
set_fact:
ca_cert_request_id: >-
{{ (ca_cert_signing_request_results.stdout|from_json).requestID }}
- name: 'RENEW | CA_MANAGER | generate json get request'
set_fact:
ca_cert_get_request:
type: 'get_certificate'
requestID: '{{ ca_cert_request_id }}'
- name: 'RENEW | CA_MANAGER | prompt for signature'
debug:
msg: >-
Please manually confirm sign request with id {{ ca_cert_request_id }}.
- name: 'RENEW | CA_MANAGER | send get request'
raw: '{{ ca_cert_get_request | to_json }}'
delegate_to: '{{ ca_cert_ca_manager_host }}'
delegate_facts: true
register: ca_cert_get_request_results
failed_when: (ca_cert_get_request_results.stdout|from_json).failed
- name: 'RENEW | store new certificate'
copy:
content: '{{ (ca_cert_get_request_results.stdout|from_json).result }}'
dest: >-
{% if ca_cert_proto == 'tls' %}{{ ca_cert_tls_cert_path }}{%
elif ca_cert_proto == 'ssh' %}{{ ca_cert_ssh_key_path }}-cert.pub{% endif %}
rescue:
- name: 'RENEW FAILED | restore backup'
copy:
remote_src: true
src: '{{ item.dest }}'
dest: '{{ item.src }}'
when: not item.failed
loop: '{{ ca_cert_key_backup.results }}'
always:
- name: 'RENEW | clean backup'
file:
path: '{{ item.dest }}'
state: 'absent'
when: not item.failed
loop: '{{ ca_cert_key_backup.results }}'
when: ca_cert_cert_is_valid.changed or ca_cert_cert_remaining_days|int < ca_cert_min_days_validity
...

+ 2
- 2
roles/ldap/defaults/main.yaml View File

@ -6,8 +6,8 @@ ldap_organization: '{{ organization }}'
ldap_check_tree: true
ldap_tls_enabled: true
ldap_tls_server_ca: '{{ tls_root_ca }}'
ldap_tls_user_ca: '{{ tls_root_ca }}'
ldap_tls_server_ca: '{{ tls_intermediate_server_ca }}'
ldap_tls_user_ca: '{{ tls_intermediate_user_ca }}'
ldap_server_accounts:
- 'projects.dmz.{{ domain }}'


+ 19
- 46
roles/ldap/tasks/4_setup_tls.yaml View File

@ -1,3 +1,4 @@
---
- name: 'install openssl'
apt:
pkg: 'openssl'
@ -7,68 +8,39 @@
tags:
- 'packages'
- name: 'create slapd private key'
shell:
cmd: >
openssl genpkey
-algorithm ED25519
-out /etc/ldap/slapd.key
creates: '/etc/ldap/slapd.key'
tags:
- 'tls_int'
- name: 'set private key ownership'
file:
path: '/etc/ldap/slapd.key'
owner: 'openldap'
group: 'openldap'
mode: '600'
- name: 'update tls server ca'
copy:
content: '{{ ldap_tls_server_ca }}'
content: '{{ ldap_tls_server_ca }}{{ tls_root_ca }}'
dest: '/etc/ldap/server_ca.crt'
tags:
- 'tls_int'
- name: 'update tls user ca'
copy:
content: '{{ ldap_tls_user_ca }}'
content: '{{ ldap_tls_user_ca }}{{ tls_root_ca }}'
dest: '/etc/ldap/user_ca.crt'
- name: 'check slapd cert against server ca'
command: >
openssl verify
-CAfile /etc/ldap/server_ca.crt
-untrusted /etc/ldap/slapd.crt
/etc/ldap/slapd.crt
register: slapd_cert_is_valid
changed_when: false
failed_when: false
tags:
- 'tls_int'
- name: 'create slapd cert request'
shell:
cmd: >
openssl req
-new
-subj '{{ openssl_x509_prefix }}/OU=Server/CN={{ host_fqdn }}'
-key /etc/ldap/slapd.key
-out /etc/ldap/slapd.csr
when: slapd_cert_is_valid.rc != 0
tags:
- 'tls_int'
- import_tasks: 'ca-signing-request.yaml'
- name: 'generete and sign slapd tls certificate'
import_role: name='ca_cert'
vars:
host: '{{ host_fqdn }}'
request_path: '/etc/ldap/slapd.csr'
output_path: '/etc/ldap/slapd.crt'
when: slapd_cert_is_valid.rc != 0
ca_cert_common_name: '{{ host_fqdn }}'
ca_cert_proto: 'tls'
ca_cert_tls_ca_path: '/etc/ldap/server_ca.crt'
ca_cert_tls_key_path: '/etc/ldap/slapd.key'
ca_cert_tls_cert_path: '/etc/ldap/slapd.crt'
ca_cert_tls_csr_path: '/etc/ldap/slapd.csr'
tags:
- 'tls_int'
- name: 'set private key ownership'
file:
path: '/etc/ldap/slapd.key'
owner: 'openldap'
group: 'openldap'
mode: '600'
# !BUG! Fixed in Ansible dev using ldap_attrs instead of ldap_attr
# Setting the parameters twice in a row fix the problem.
# Ref: https://github.com/ansible/ansible/issues/25665
@ -110,3 +82,4 @@
notify: 'restart slapd'
tags:
- 'tls_int'
...

+ 9
- 42
roles/openvpn/tasks/main.yaml View File

@ -6,18 +6,6 @@
tags:
- 'packages'
- name: 'create openvpn private key'
shell:
cmd: >
openssl genpkey
-algorithm ed25519
-out /etc/openvpn/openvpn.key
args:
creates: '/etc/openvpn/openvpn.key'
notify: 'reload openvpn'
tags:
- 'tls_int'
# Shouldn't be required for TLSv1.3
#
#- name: create openvpn dh2048
@ -41,37 +29,15 @@
tags:
- 'tls_int'
- name: 'check openvpn cert status'
command: >-
openssl verify
-CAfile /etc/openvpn/server_ca.crt
/etc/openvpn/openvpn.crt
register: openvpn_cert_is_valid
changed_when: false
failed_when: false
tags:
- 'tls_int'
- name: 'create openvpn cert request'
shell: >
openssl req
-new
-subj "{{ openssl_x509_prefix }}/OU=Server/CN={{ host_fqdn }}"
-key /etc/openvpn/openvpn.key
-out /etc/openvpn/openvpn.csr
when: openvpn_cert_is_valid.rc != 0
tags:
- 'tls_int'
- import_tasks: 'ca-signing-request.yaml'
- name: 'generate and sign server certificate'
import_role: name='ca_cert'
vars:
host: '{{ host_fqdn }}'
request_path: '/etc/openvpn/openvpn.csr'
output_path: '/etc/openvpn/openvpn.crt'
when: openvpn_cert_is_valid.rc != 0
notify: 'reload openvpn'
tags:
- 'tls_int'
ca_cert_common_name: '{{ host_fqdn }}'
ca_cert_proto: 'tls'
ca_cert_tls_ca_path: '/etc/openvpn/server_ca.crt'
ca_cert_tls_key_path: '/etc/openvpn/openvpn.key'
ca_cert_tls_csr_path: '/etc/openvpn/openvpn.csr'
ca_cert_tls_cert_path: '/etc/openvpn/openvpn.crt'
- name: 'write openvpn configuration'
template:
@ -87,3 +53,4 @@
shell: 'uci commit openvpn'
notify: 'reload openvpn'
when: config_updated.changed
...

+ 26
- 80
roles/ssh_server/tasks/main.yaml View File

@ -1,108 +1,54 @@
---
- include: 'roles/service/tasks/main.yaml'
- import_role: name='service'
vars:
service_name: 'ssh'
service_packages:
- 'openssh-server'
- 'openssh-sftp-server'
- name: 'update user ca certs'
template:
src: 'user_ca.pub.j2'
dest: '/etc/ssh/user_ca.pub'
- name: 'upload user and server ca'
copy:
content: |
{% for ca in item.1 %}
{{ ca }}
{% endfor %}
dest: '/etc/ssh/{{ item.0 }}_ca.pub'
vars:
cas: '{{ item.1 }}'
notify: 'restart ssh'
loop:
- [ 'user', '{{ ssh_user_ca }}' ]
- [ 'server', '{{ ssh_server_ca }}' ]
tags:
- ssh_certs
- name: 'validate ssh cert if present'
ssh_cert:
register: ssh_verification
ignore_errors: yes
tags:
- ssh_certs
- debug:
var: ssh_verification
verbosity: 2
tags:
- ssh_certs
- block:
- name: 'generate host cert request'
cert_request:
host: '{{ host_fqdn }}'
path: '/etc/ssh/ssh_host_ed25519_key.pub'
proto: 'ssh'
register: ca_request
- name: 'start sign request'
include: 'ca-dialog.yaml'
vars:
ansible_connection: 'ssh'
- debug:
var: request_result
verbosity: 2
- set_fact:
request_output: '{{ request_result.stdout | from_json }}'
- 'ssh_certs'
- debug:
var: request_output
verbosity: 2
- name: 'generate get request'
set_fact:
ca_request:
type: 'get_certificate'
requestID: '{{ request_output.requestID }}'
- debug:
var: ca_request
verbosity: 2
- debug:
msg: 'Please manualy confirm sign request with id {{ request_output.requestID }}'
- name: 'wait for cert'
include: 'ca-dialog.yaml'
vars:
ansible_connection: 'ssh'
- debug:
var: request_result
verbosity: 2
- set_fact:
cert_key: '{{ request_result.stdout | string | from_json }}'
- name: 'write certificate to container'
copy:
content: '{{ cert_key.result }}'
dest: '/etc/ssh/ssh_host_ed25519_key-cert.pub'
register: set_pub_key
notify: 'restart ssh'
when: ssh_verification.failed
- name: 'generate and sign host certificate'
import_role: name='ca_cert'
vars:
ca_cert_common_name: '{{ host_fqdn }}'
ca_cert_proto: 'ssh'
ca_cert_ssh_ca_path: '/etc/ssh/server_ca.pub'
ca_cert_ssh_key_path: '/etc/ssh/ssh_host_ed25519_key'
tags:
- ssh_certs
- 'ssh_certs'
- name: 'add certificate to sshd config'
- name: 'add host certificate to sshd config'
lineinfile:
line: 'HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub'
dest: '/etc/ssh/sshd_config'
regexp: '^HostCertificate *'
notify: 'restart ssh'
tags:
- ssh_certs
- 'ssh_certs'
- name: 'trust user ca key'
- name: 'add user ca to sshd config'
lineinfile:
line: 'TrustedUserCAKeys /etc/ssh/user_ca.pub'
dest: '/etc/ssh/sshd_config'
regexp: '^TrustedUserCAKeys *'
notify: 'restart ssh'
tags:
- ssh_certs
- 'ssh_certs'
- name: 'permit root login only with certificate'
lineinfile:


+ 0
- 4
roles/ssh_server/templates/user_ca.pub.j2 View File

@ -1,4 +0,0 @@
{% for key in ssh_user_ca %}
{{ key }}
{% endfor %}

+ 2
- 9
tasks/ca-dialog.yaml View File

@ -1,17 +1,10 @@
### DEPRECATED -> import role `ca_cert` instead
- debug:
msg: "Sending certificate request to {{ hostvars | ip_from_inventory('authorities_request') }}"
- debug:
var: ca_request
verbosity: 2
msg: "Warning, deprecated"
- name: 'CA_MANAGER | sending request to ca...'
raw: '{{ ca_request | to_json }}'
delegate_to: 'authorities_request'
delegate_facts: True
register: request_result
- debug:
var: request_result
verbosity: 2
failed_when: '( request_result.stdout | from_json ).failed'

+ 0
- 42
tasks/ca-signing-request.yaml View File

@ -1,42 +0,0 @@
---
- name: 'CA_MANAGER | generating json signing request'
cert_request:
host: '{{ host }}'
path: '{{ request_path }}'
proto: 'ssl'
client: '{{ client | default(false) }}'
register: ca_request
- name: 'CA_MANAGER | sending json signing request'
import_tasks: 'ca-dialog.yaml'
- name: 'CA_MANAGER | read ca answer'
set_fact:
request_output: '{{ request_result.stdout | string | from_json }}'
- debug:
var: request_result
- name: 'CA_MANAGER | generating json get request'
set_fact:
ca_request:
type: 'get_certificate'
requestID: '{{ request_output.requestID }}'
- name: 'CA_MANAGER | print request id'
debug:
msg: >
Please manually confirm sign request with id
{{ request_output.requestID }}
- name: 'CA_MANAGER | waiting for certificate...'
include: 'ca-dialog.yaml'
- name: 'CA MANAGER | read certificate'
set_fact:
cert_key: '{{ request_result.stdout | string | from_json }}'
- name: 'CA_MANAGER | saving certificate'
copy:
content: '{{ cert_key.result }}'
dest: '{{ output_path }}'

Loading…
Cancel
Save