parent
20b95adf2b
commit
eb7f6a5e62
9 changed files with 394 additions and 0 deletions
315
lib/ansible/modules/crypto/openssh_keypair.py
Normal file
315
lib/ansible/modules/crypto/openssh_keypair.py
Normal file
|
@ -0,0 +1,315 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: openssh_keypair
|
||||
author: "David Kainz (@lolcube)"
|
||||
version_added: "2.8"
|
||||
short_description: Generate OpenSSH private and public keys.
|
||||
description:
|
||||
- "This module allows one to (re)generate OpenSSH private and public keys. It uses
|
||||
ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519)
|
||||
or C(ecdsa) private keys."
|
||||
requirements:
|
||||
- "ssh-keygen"
|
||||
options:
|
||||
state:
|
||||
required: false
|
||||
default: present
|
||||
choices: [ present, absent ]
|
||||
description:
|
||||
- Whether the private and public keys should exist or not, taking action if the state is different from what is stated.
|
||||
size:
|
||||
required: false
|
||||
description:
|
||||
- "Specifies the number of bits in the private key to create. For RSA keys, the minimum size is 1024 bits and the default is 4096 bits.
|
||||
Generally, 2048 bits is considered sufficient. DSA keys must be exactly 1024 bits as specified by FIPS 186-2.
|
||||
For ECDSA keys, size determines the key length by selecting from one of three elliptic curve sizes: 256, 384 or 521 bits.
|
||||
Attempting to use bit lengths other than these three values for ECDSA keys will cause this module to fail.
|
||||
Ed25519 keys have a fixed length and the size will be ignored."
|
||||
type:
|
||||
required: false
|
||||
default: rsa
|
||||
choices: ['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']
|
||||
description:
|
||||
- "The algorithm used to generate the SSH private key. C(rsa1) is for protocol version 1.
|
||||
C(rsa1) is deprecated and may not be supported by every version of ssh-keygen."
|
||||
force:
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
description:
|
||||
- Should the key be regenerated even if it already exists
|
||||
path:
|
||||
required: true
|
||||
description:
|
||||
- Name of the files containing the public and private key. The file containing the public key will have the extension C(.pub).
|
||||
comment:
|
||||
required: false
|
||||
description:
|
||||
- Provides a new comment to the public key. When checking if the key is in the correct state this will be ignored.
|
||||
|
||||
extends_documentation_fragment: files
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Generate an OpenSSH keypair with the default values (4096 bits, rsa)
|
||||
- openssh_keypair:
|
||||
path: /tmp/id_ssh_rsa
|
||||
|
||||
# Generate an OpenSSH rsa keypair with a different size (2048 bits)
|
||||
- openssh_keypair:
|
||||
path: /tmp/id_ssh_rsa
|
||||
size: 2048
|
||||
|
||||
# Force regenerate an OpenSSH keypair if it already exists
|
||||
- openssh_keypair:
|
||||
path: /tmp/id_ssh_rsa
|
||||
force: True
|
||||
|
||||
# Generate an OpenSSH keypair with a different algorithm (dsa)
|
||||
- openssh_keypair:
|
||||
path: /tmp/id_ssh_dsa
|
||||
type: dsa
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
size:
|
||||
description: Size (in bits) of the SSH private key
|
||||
returned: changed or success
|
||||
type: int
|
||||
sample: 4096
|
||||
type:
|
||||
description: Algorithm used to generate the SSH private key
|
||||
returned: changed or success
|
||||
type: string
|
||||
sample: rsa
|
||||
filename:
|
||||
description: Path to the generated SSH private key file
|
||||
returned: changed or success
|
||||
type: string
|
||||
sample: /tmp/id_ssh_rsa
|
||||
fingerprint:
|
||||
description: The fingerprint of the key.
|
||||
returned: changed or success
|
||||
type: string
|
||||
sample: 4096 SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM example@example.com (RSA)
|
||||
'''
|
||||
|
||||
import os
|
||||
import errno
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
class KeypairError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Keypair(object):
|
||||
|
||||
def __init__(self, module):
|
||||
self.path = module.params['path']
|
||||
self.state = module.params['state']
|
||||
self.force = module.params['force']
|
||||
self.size = module.params['size']
|
||||
self.type = module.params['type']
|
||||
self.comment = module.params['comment']
|
||||
self.changed = False
|
||||
self.check_mode = module.check_mode
|
||||
self.privatekey = None
|
||||
self.fingerprint = {}
|
||||
|
||||
if self.type in ('rsa', 'rsa1'):
|
||||
self.size = 4096 if self.size is None else self.size
|
||||
if self.size < 1024:
|
||||
module.fail_json(msg=('For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. '
|
||||
'Attempting to use bit lengths under 1024 will cause the module to fail.'))
|
||||
|
||||
if self.type == 'dsa':
|
||||
self.size = 1024 if self.size is None else self.size
|
||||
if self.size != 1024:
|
||||
module.fail_json(msg=('DSA keys must be exactly 1024 bits as specified by FIPS 186-2.'))
|
||||
|
||||
if self.type == 'ecdsa':
|
||||
self.size = 256 if self.size is None else self.size
|
||||
if self.size not in (256, 384, 521):
|
||||
module.fail_json(msg=('For ECDSA keys, size determines the key length by selecting from '
|
||||
'one of three elliptic curve sizes: 256, 384 or 521 bits. '
|
||||
'Attempting to use bit lengths other than these three values for '
|
||||
'ECDSA keys will cause this module to fail. '))
|
||||
if self.type == 'ed25519':
|
||||
self.size = 256
|
||||
|
||||
def generate(self, module):
|
||||
# generate a keypair
|
||||
if not self.isValid(module, perms_required=False) or self.force:
|
||||
args = [
|
||||
module.get_bin_path('ssh-keygen', True),
|
||||
'-q',
|
||||
'-N', '',
|
||||
'-b', str(self.size),
|
||||
'-t', self.type,
|
||||
'-f', self.path,
|
||||
]
|
||||
|
||||
if self.comment:
|
||||
args.extend(['-C', self.comment])
|
||||
else:
|
||||
args.extend(['-C', ""])
|
||||
|
||||
try:
|
||||
self.changed = True
|
||||
module.run_command(args)
|
||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path])
|
||||
self.fingerprint = proc[1].split()
|
||||
except Exception as e:
|
||||
self.remove()
|
||||
module.fail_json(msg="%s" % to_native(e))
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
self.changed = True
|
||||
|
||||
def isValid(self, module, perms_required=True):
|
||||
|
||||
# check if the key is correct
|
||||
def _check_state():
|
||||
return os.path.exists(self.path)
|
||||
|
||||
if _check_state():
|
||||
proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path])
|
||||
fingerprint = proc[1].split()
|
||||
keysize = int(fingerprint[0])
|
||||
keytype = fingerprint[-1][1:-1].lower()
|
||||
else:
|
||||
return False
|
||||
|
||||
def _check_perms(module):
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
return not module.set_fs_attributes_if_different(file_args, False)
|
||||
|
||||
def _check_type():
|
||||
return self.type == keytype
|
||||
|
||||
def _check_size():
|
||||
return self.size == keysize
|
||||
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
if not perms_required:
|
||||
return _check_state() and _check_type() and _check_size()
|
||||
|
||||
return _check_state() and _check_perms(module) and _check_type() and _check_size()
|
||||
|
||||
def dump(self):
|
||||
# return result as a dict
|
||||
|
||||
"""Serialize the object into a dictionary."""
|
||||
|
||||
result = {
|
||||
'changed': self.changed,
|
||||
'size': self.size,
|
||||
'type': self.type,
|
||||
'filename': self.path,
|
||||
'fingerprint': self.fingerprint,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def remove(self):
|
||||
"""Remove the resource from the filesystem."""
|
||||
|
||||
try:
|
||||
os.remove(self.path)
|
||||
self.changed = True
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise KeypairError(exc)
|
||||
else:
|
||||
pass
|
||||
|
||||
if os.path.exists(self.path + ".pub"):
|
||||
try:
|
||||
os.remove(self.path + ".pub")
|
||||
self.changed = True
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise KeypairError(exc)
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# Define Ansible Module
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(default='present', choices=['present', 'absent'], type='str'),
|
||||
size=dict(type='int'),
|
||||
type=dict(default='rsa', choices=['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519'], type='str'),
|
||||
force=dict(default=False, type='bool'),
|
||||
path=dict(required=True, type='path'),
|
||||
comment=dict(type='str'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
add_file_common_args=True,
|
||||
)
|
||||
|
||||
# Check if Path exists
|
||||
base_dir = os.path.dirname(module.params['path'])
|
||||
if not os.path.isdir(base_dir):
|
||||
module.fail_json(
|
||||
name=base_dir,
|
||||
msg='The directory %s does not exist or the file is not a directory' % base_dir
|
||||
)
|
||||
|
||||
keypair = Keypair(module)
|
||||
|
||||
if keypair.state == 'present':
|
||||
|
||||
if module.check_mode:
|
||||
result = keypair.dump()
|
||||
result['changed'] = module.params['force'] or not keypair.isValid(module)
|
||||
module.exit_json(**result)
|
||||
|
||||
try:
|
||||
keypair.generate(module)
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
else:
|
||||
|
||||
if module.check_mode:
|
||||
keypair.changed = os.path.exists(module.params['path'])
|
||||
if keypair.changed:
|
||||
keypair.fingerprint = {}
|
||||
result = keypair.dump()
|
||||
module.exit_json(**result)
|
||||
|
||||
try:
|
||||
keypair.remove()
|
||||
except Exception as exc:
|
||||
module.fail_json(msg=to_native(exc))
|
||||
|
||||
result = keypair.dump()
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
2
test/integration/targets/openssh_keypair/aliases
Normal file
2
test/integration/targets/openssh_keypair/aliases
Normal file
|
@ -0,0 +1,2 @@
|
|||
shippable/posix/group1
|
||||
destructive
|
2
test/integration/targets/openssh_keypair/meta/main.yml
Normal file
2
test/integration/targets/openssh_keypair/meta/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
dependencies:
|
||||
- setup_ssh_keygen
|
25
test/integration/targets/openssh_keypair/tasks/main.yml
Normal file
25
test/integration/targets/openssh_keypair/tasks/main.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
- name: Generate privatekey1 - standard
|
||||
connection: local
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey1'
|
||||
|
||||
- name: Generate privatekey2 - size 2048
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey2'
|
||||
size: 2048
|
||||
|
||||
- name: Generate privatekey3 - type dsa
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey3'
|
||||
type: dsa
|
||||
|
||||
- name: Generate privatekey4 - standard
|
||||
openssh_keypair:
|
||||
path: '{{ output_dir }}/privatekey4'
|
||||
|
||||
- name: Delete privatekey4 - standard
|
||||
openssh_keypair:
|
||||
state: absent
|
||||
path: '{{ output_dir }}/privatekey4'
|
||||
|
||||
- import_tasks: ../tests/validate.yml
|
39
test/integration/targets/openssh_keypair/tests/validate.yml
Normal file
39
test/integration/targets/openssh_keypair/tests/validate.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
- name: Validate privatekey1 (test - RSA key with size 4096 bits)
|
||||
shell: "ssh-keygen -lf {{ output_dir }}/privatekey1 | grep -o -E '^[0-9]+'"
|
||||
register: privatekey1
|
||||
|
||||
- name: Validate privatekey1 (assert - RSA key with size 4096 bits)
|
||||
assert:
|
||||
that:
|
||||
- privatekey1.stdout == '4096'
|
||||
|
||||
|
||||
- name: Validate privatekey2 (test - RSA key with size 2048 bits)
|
||||
shell: "ssh-keygen -lf {{ output_dir }}/privatekey2 | grep -o -E '^[0-9]+'"
|
||||
register: privatekey2
|
||||
|
||||
- name: Validate privatekey2 (assert - RSA key with size 2048 bits)
|
||||
assert:
|
||||
that:
|
||||
- privatekey2.stdout == '2048'
|
||||
|
||||
|
||||
- name: Validate privatekey3 (test - DSA key with size 1024 bits)
|
||||
shell: "ssh-keygen -lf {{ output_dir }}/privatekey3 | grep -o -E '^[0-9]+'"
|
||||
register: privatekey3
|
||||
|
||||
- name: Validate privatekey3 (assert - DSA key with size 4096 bits)
|
||||
assert:
|
||||
that:
|
||||
- privatekey3.stdout == '1024'
|
||||
|
||||
|
||||
- name: Validate privatekey4 (test - Ensure key has been removed)
|
||||
stat:
|
||||
path: '{{ output_dir }}/privatekey4'
|
||||
register: privatekey4
|
||||
|
||||
- name: Validate privatekey4 (assert - Ensure key has been removed)
|
||||
assert:
|
||||
that:
|
||||
- privatekey4.stat.exists == False
|
8
test/integration/targets/setup_ssh_keygen/tasks/main.yml
Normal file
8
test/integration/targets/setup_ssh_keygen/tasks/main.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- name: Include OS-specific variables
|
||||
include_vars: '{{ ansible_os_family }}.yml'
|
||||
when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD"
|
||||
|
||||
- name: Install ssh-keygen
|
||||
package:
|
||||
name: '{{ openssh_client_package_name }}'
|
||||
when: not ansible_os_family == "Darwin" and not ansible_os_family == "FreeBSD"
|
|
@ -0,0 +1 @@
|
|||
openssh_client_package_name: openssh-client
|
|
@ -0,0 +1 @@
|
|||
openssh_client_package_name: openssh-clients
|
1
test/integration/targets/setup_ssh_keygen/vars/Suse.yml
Normal file
1
test/integration/targets/setup_ssh_keygen/vars/Suse.yml
Normal file
|
@ -0,0 +1 @@
|
|||
openssh_client_package_name: openssh
|
Loading…
Reference in a new issue