Implement use_agent option to get signing key from ssh-agent. (#117)

pull/131/head
Doug Stanley 2020-10-19 12:07:36 -04:00 committed by GitHub
parent a6490fa60e
commit b32adcce78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 192 additions and 0 deletions

View File

@ -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).

View File

@ -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

View File

@ -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):

View File

@ -1,2 +1,3 @@
dependencies: dependencies:
- setup_ssh_keygen - setup_ssh_keygen
- setup_ssh_agent

View File

@ -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'

View File

@ -0,0 +1,2 @@
dependencies:
- setup_ssh_keygen

View File

@ -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() }}"