Implement use_agent option to get signing key from ssh-agent. (#117)
parent
a6490fa60e
commit
b32adcce78
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- openssh_cert - add module parameter ``use_agent`` to enable using signing keys stored in ssh-agent (https://github.com/ansible-collections/community.crypto/issues/116).
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def parse_openssh_version(version_string):
|
||||||
|
"""Parse the version output of ssh -V and return version numbers that can be compared"""
|
||||||
|
|
||||||
|
parsed_result = re.match(
|
||||||
|
r"^.*openssh_(?P<version>[0-9.]+)(p?[0-9]+)[^0-9]*.*$", version_string.lower()
|
||||||
|
)
|
||||||
|
if parsed_result is not None:
|
||||||
|
version = parsed_result.group("version").strip()
|
||||||
|
else:
|
||||||
|
version = None
|
||||||
|
|
||||||
|
return version
|
|
@ -53,6 +53,12 @@ options:
|
||||||
- If this is set, I(signing_key) needs to point to a file containing the public key of the CA.
|
- If this is set, I(signing_key) needs to point to a file containing the public key of the CA.
|
||||||
type: str
|
type: str
|
||||||
version_added: 1.1.0
|
version_added: 1.1.0
|
||||||
|
use_agent:
|
||||||
|
description:
|
||||||
|
- Should the ssh-keygen use a CA key residing in a ssh-agent.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
version_added: 1.3.0
|
||||||
public_key:
|
public_key:
|
||||||
description:
|
description:
|
||||||
- The path to the public key that will be signed with the signing key in order to generate the certificate.
|
- The path to the public key that will be signed with the signing key in order to generate the certificate.
|
||||||
|
@ -216,12 +222,14 @@ import tempfile
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import MINYEAR, MAXYEAR
|
from datetime import MINYEAR, MAXYEAR
|
||||||
|
from distutils.version import LooseVersion
|
||||||
from shutil import copy2, rmtree
|
from shutil import copy2, rmtree
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from ansible.module_utils._text import to_native
|
from ansible.module_utils._text import to_native
|
||||||
|
|
||||||
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
|
||||||
|
from ansible_collections.community.crypto.plugins.module_utils.crypto.openssh import parse_openssh_version
|
||||||
|
|
||||||
|
|
||||||
class CertificateError(Exception):
|
class CertificateError(Exception):
|
||||||
|
@ -235,6 +243,7 @@ class Certificate(object):
|
||||||
self.force = module.params['force']
|
self.force = module.params['force']
|
||||||
self.type = module.params['type']
|
self.type = module.params['type']
|
||||||
self.signing_key = module.params['signing_key']
|
self.signing_key = module.params['signing_key']
|
||||||
|
self.use_agent = module.params['use_agent']
|
||||||
self.pkcs11_provider = module.params['pkcs11_provider']
|
self.pkcs11_provider = module.params['pkcs11_provider']
|
||||||
self.public_key = module.params['public_key']
|
self.public_key = module.params['public_key']
|
||||||
self.path = module.params['path']
|
self.path = module.params['path']
|
||||||
|
@ -273,6 +282,9 @@ class Certificate(object):
|
||||||
if self.pkcs11_provider:
|
if self.pkcs11_provider:
|
||||||
args.extend(['-D', self.pkcs11_provider])
|
args.extend(['-D', self.pkcs11_provider])
|
||||||
|
|
||||||
|
if self.use_agent:
|
||||||
|
args.extend(['-U'])
|
||||||
|
|
||||||
validity = ""
|
validity = ""
|
||||||
|
|
||||||
if not (self.valid_from == "always" and self.valid_to == "forever"):
|
if not (self.valid_from == "always" and self.valid_to == "forever"):
|
||||||
|
@ -547,6 +559,7 @@ def main():
|
||||||
force=dict(type='bool', default=False),
|
force=dict(type='bool', default=False),
|
||||||
type=dict(type='str', choices=['host', 'user']),
|
type=dict(type='str', choices=['host', 'user']),
|
||||||
signing_key=dict(type='path'),
|
signing_key=dict(type='path'),
|
||||||
|
use_agent=dict(type='bool', default=False),
|
||||||
pkcs11_provider=dict(type='str'),
|
pkcs11_provider=dict(type='str'),
|
||||||
public_key=dict(type='path'),
|
public_key=dict(type='path'),
|
||||||
path=dict(type='path', required=True),
|
path=dict(type='path', required=True),
|
||||||
|
@ -563,6 +576,21 @@ def main():
|
||||||
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if module.params['use_agent']:
|
||||||
|
ssh = module.get_bin_path('ssh', True)
|
||||||
|
proc = module.run_command([ssh, '-Vq'])
|
||||||
|
ssh_version_string = proc[2].strip()
|
||||||
|
ssh_version = parse_openssh_version(ssh_version_string)
|
||||||
|
if ssh_version is None:
|
||||||
|
module.fail_json(msg="Failed to parse ssh version")
|
||||||
|
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
|
||||||
|
module.fail_json(
|
||||||
|
msg=(
|
||||||
|
"Signing with CA key in ssh agent requires ssh 7.6 or newer."
|
||||||
|
" Your version is: %s"
|
||||||
|
) % ssh_version_string
|
||||||
|
)
|
||||||
|
|
||||||
def isBaseDir(path):
|
def isBaseDir(path):
|
||||||
base_dir = os.path.dirname(path) or '.'
|
base_dir = os.path.dirname(path) or '.'
|
||||||
if not os.path.isdir(base_dir):
|
if not os.path.isdir(base_dir):
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
- setup_ssh_keygen
|
- setup_ssh_keygen
|
||||||
|
- setup_ssh_agent
|
||||||
|
|
|
@ -411,3 +411,83 @@
|
||||||
path: '{{ output_dir }}/id_key'
|
path: '{{ output_dir }}/id_key'
|
||||||
state: absent
|
state: absent
|
||||||
check_mode: yes
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: openssh_cert integration tests that require ssh-agent
|
||||||
|
when: openssh_version is version("7.6",">=")
|
||||||
|
environment:
|
||||||
|
SSH_AUTH_SOCK: "{{ openssh_agent_sock }}"
|
||||||
|
block:
|
||||||
|
- name: Generate keypair for agent tests
|
||||||
|
openssh_keypair:
|
||||||
|
path: '{{ output_dir }}/id_key'
|
||||||
|
type: rsa
|
||||||
|
- name: Generate always valid cert using agent without key in agent (should fail)
|
||||||
|
openssh_cert:
|
||||||
|
type: user
|
||||||
|
signing_key: '{{ output_dir }}/id_key'
|
||||||
|
public_key: '{{ output_dir }}/id_key.pub'
|
||||||
|
path: '{{ output_dir }}/id_cert_with_agent'
|
||||||
|
use_agent: yes
|
||||||
|
valid_from: always
|
||||||
|
valid_to: forever
|
||||||
|
register: rc_no_key_in_agent
|
||||||
|
ignore_errors: yes
|
||||||
|
- name: Make sure cert creation with agent fails if key not in agent
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- rc_no_key_in_agent is failed
|
||||||
|
- "'agent contains no identities' in rc_no_key_in_agent.msg or 'not found in agent' in rc_no_key_in_agent.msg"
|
||||||
|
- name: Add key to agent
|
||||||
|
command: 'ssh-add {{ output_dir }}/id_key'
|
||||||
|
- name: Generate always valid cert with agent (check mode)
|
||||||
|
openssh_cert:
|
||||||
|
type: user
|
||||||
|
signing_key: '{{ output_dir }}/id_key'
|
||||||
|
public_key: '{{ output_dir }}/id_key.pub'
|
||||||
|
path: '{{ output_dir }}/id_cert_with_agent'
|
||||||
|
use_agent: yes
|
||||||
|
valid_from: always
|
||||||
|
valid_to: forever
|
||||||
|
check_mode: yes
|
||||||
|
- name: Generate always valid cert with agent
|
||||||
|
openssh_cert:
|
||||||
|
type: user
|
||||||
|
signing_key: '{{ output_dir }}/id_key'
|
||||||
|
public_key: '{{ output_dir }}/id_key.pub'
|
||||||
|
path: '{{ output_dir }}/id_cert_with_agent'
|
||||||
|
use_agent: yes
|
||||||
|
valid_from: always
|
||||||
|
valid_to: forever
|
||||||
|
- name: Generate always valid cert with agent (idempotent)
|
||||||
|
openssh_cert:
|
||||||
|
type: user
|
||||||
|
signing_key: '{{ output_dir }}/id_key'
|
||||||
|
public_key: '{{ output_dir }}/id_key.pub'
|
||||||
|
path: '{{ output_dir }}/id_cert_with_agent'
|
||||||
|
use_agent: yes
|
||||||
|
valid_from: always
|
||||||
|
valid_to: forever
|
||||||
|
register: rc_cert_with_agent_idempotent
|
||||||
|
- name: Check agent idempotency
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- rc_cert_with_agent_idempotent is not changed
|
||||||
|
msg: OpenSSH certificate generation without serial number is idempotent.
|
||||||
|
- name: Generate always valid cert with agent (idempotent, check mode)
|
||||||
|
openssh_cert:
|
||||||
|
type: user
|
||||||
|
signing_key: '{{ output_dir }}/id_key'
|
||||||
|
public_key: '{{ output_dir }}/id_key.pub'
|
||||||
|
path: '{{ output_dir }}/id_cert_with_agent'
|
||||||
|
use_agent: yes
|
||||||
|
valid_from: always
|
||||||
|
valid_to: forever
|
||||||
|
check_mode: yes
|
||||||
|
- name: Remove keypair for agent tests
|
||||||
|
openssh_keypair:
|
||||||
|
path: '{{ output_dir }}/id_key'
|
||||||
|
state: absent
|
||||||
|
- name: Remove certificate
|
||||||
|
openssh_cert:
|
||||||
|
state: absent
|
||||||
|
path: '{{ output_dir }}/id_cert_with_agent'
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
dependencies:
|
||||||
|
- setup_ssh_keygen
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
####################################################################
|
||||||
|
# WARNING: These are designed specifically for Ansible tests #
|
||||||
|
# and should not be used as examples of how to write Ansible roles #
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
- name: Start an ssh agent to use for tests
|
||||||
|
shell: eval $(ssh-agent)>/dev/null&&echo "${SSH_AGENT_PID};${SSH_AUTH_SOCK}"
|
||||||
|
register: openssh_agent_env_vars
|
||||||
|
|
||||||
|
- name: Register ssh agent facts
|
||||||
|
set_fact:
|
||||||
|
openssh_agent_pid: "{{ openssh_agent_env_vars.stdout.split(';')[0] }}"
|
||||||
|
openssh_agent_sock: "{{ openssh_agent_env_vars.stdout.split(';')[1] }}"
|
||||||
|
|
||||||
|
- name: stat agent socket
|
||||||
|
stat:
|
||||||
|
path: "{{ openssh_agent_sock }}"
|
||||||
|
register: openssh_agent_socket_stat
|
||||||
|
|
||||||
|
- name: Assert agent socket file is a socket
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- openssh_agent_socket_stat.stat.issock is defined
|
||||||
|
- openssh_agent_socket_stat.stat.issock
|
||||||
|
fail_msg: "{{ openssh_agent_sock }} is not a socket"
|
||||||
|
|
||||||
|
- name: Verify agent responds
|
||||||
|
command: ssh-add -l
|
||||||
|
register: rc_openssh_agent_ssh_add_check
|
||||||
|
environment:
|
||||||
|
SSH_AUTH_SOCK: "{{ openssh_agent_sock }}"
|
||||||
|
when: openssh_agent_socket_stat.stat.issock
|
||||||
|
failed_when: rc_openssh_agent_ssh_add_check.rc == 2
|
||||||
|
|
||||||
|
- name: Get ssh version
|
||||||
|
shell: ssh -Vq 2>&1|sed 's/^.*OpenSSH_\([0-9]\{1,\}\.[0-9]\{1,\}\).*$/\1/'
|
||||||
|
register:
|
||||||
|
rc_openssh_version_output
|
||||||
|
|
||||||
|
- name: Set ssh version facts
|
||||||
|
set_fact:
|
||||||
|
openssh_version: "{{ rc_openssh_version_output.stdout.strip() }}"
|
Loading…
Reference in New Issue