Playbooks to a new Lilik
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

161 lines
4.7 KiB

  1. #! /usr/bin/env python3
  2. from datetime import datetime
  3. import string
  4. import subprocess
  5. from ansible.module_utils.basic import AnsibleModule
  6. __doc__ = '''
  7. module: ssh_cert
  8. author: Edoardo Putti
  9. short_description: Check ssh certificate validity
  10. '''
  11. CERT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
  12. def serial(lines):
  13. for l in lines:
  14. if l.startswith('Serial'):
  15. return int(l.split().pop(), 10)
  16. def signin_ca(lines):
  17. for l in lines:
  18. if l.startswith('Signing CA'):
  19. # return l.split().pop()
  20. # Starting from OpenSSH v8 the output format of ssh-keygen
  21. # has changed, this should work for all versions:
  22. return l.split()[3]
  23. def principals(lines):
  24. principals = []
  25. reading = False
  26. for l in lines:
  27. if l.startswith('Critical Options:'):
  28. reading = False
  29. if reading:
  30. principals.append(l)
  31. if l == 'Principals:':
  32. reading = True
  33. return principals
  34. def still_valid(cert_timestamps):
  35. t = datetime.today()
  36. return t < cert_timestamps['valid']['to'] and t > cert_timestamps['valid']['from']
  37. def expired(cert_timestamps):
  38. t = datetime.today()
  39. return t > cert_timestamps['valid']['to']
  40. def not_valid(cert_timestamps):
  41. t = datetime.today()
  42. return t < cert_timestamps['valid']['from']
  43. def cert_type(lines):
  44. for l in lines:
  45. if l.startswith('Type'):
  46. return l.split(maxsplit=2)[1:]
  47. def valid_from(lines):
  48. for l in lines:
  49. if l.startswith('Valid'):
  50. return datetime.strptime(l.split()[2], CERT_TIME_FORMAT)
  51. def valid_to(lines):
  52. for l in lines:
  53. if l.startswith('Valid'):
  54. return datetime.strptime(l.split()[4], CERT_TIME_FORMAT)
  55. def main():
  56. module = AnsibleModule(
  57. argument_spec=dict(
  58. principals=dict(
  59. required=True,
  60. type='list',
  61. ),
  62. path=dict(
  63. required=False,
  64. type='str',
  65. default='/etc/ssh/ssh_host_ed25519_key-cert.pub',
  66. ),
  67. ca_path=dict(
  68. required=False,
  69. type='str',
  70. default='/etc/ssh/user_ca.pub',
  71. ),
  72. ),
  73. supports_check_mode=False,
  74. )
  75. result = {}
  76. result['rc'] = 0
  77. result['msg'] = ''
  78. result['failed'] = False
  79. result['ca'] = {}
  80. result['ca']['path'] = module.params.get('ca_path')
  81. result['certificate'] = {}
  82. result['certificate']['path'] = module.params.get('path')
  83. ca_output = subprocess.check_output([
  84. 'ssh-keygen',
  85. '-l',
  86. '-f', result['ca']['path'],
  87. ])
  88. # If multiple CA are present verify cert against the first one
  89. ca_output = ca_output.splitlines()[0]
  90. ca_lines = ca_output.decode().split(maxsplit=2)
  91. result['ca']['fingerprint'] = ca_lines[1]
  92. result['ca']['comment'] = ca_lines[2]
  93. cert_output = subprocess.check_output([
  94. 'ssh-keygen',
  95. '-L',
  96. '-f', result['certificate']['path'],
  97. ])
  98. cert_lines = [line.strip() for line in cert_output.decode().split('\n')]
  99. result['certificate']['signin_ca'] = signin_ca(cert_lines)
  100. result['certificate']['principals'] = principals(cert_lines)
  101. result['certificate']['valid'] = {
  102. 'from': valid_from(cert_lines),
  103. 'to': valid_to(cert_lines),
  104. 'remaining_days': (valid_to(cert_lines)-datetime.now()).days
  105. }
  106. if not still_valid(result['certificate']):
  107. result['failed'] = True
  108. result['msg'] += 'The certificate is not valid now. '
  109. if not_valid(result['certificate']):
  110. result['rc'] += 2
  111. if expired(result['certificate']):
  112. result['rc'] += 4
  113. result['certificate']['serial'] = serial(cert_lines)
  114. result['certificate']['type'] = cert_type(cert_lines)
  115. if not result['certificate']['signin_ca'] == result['ca']['fingerprint']:
  116. result['failed'] = True
  117. result['msg'] = 'The provided CA did not sign the certificate specified. '
  118. result['rc'] += 1
  119. principal_mismatch = False
  120. for principal in module.params.get('principals'):
  121. if not principal in result['certificate']['principals']:
  122. principal_mismatch = True
  123. result['msg'] += 'Principal {} not found in cert. '.format(principal)
  124. if principal_mismatch:
  125. result['failed'] = True
  126. result['rc'] += 8
  127. module.exit_json(**result)
  128. if __name__ == '__main__':
  129. main()