0
0
mirror of https://github.com/saltstack-formulas/openssh-formula.git synced 2025-04-19 20:02:10 +02:00

Pillar openssh.known_hosts_salt_ssh

This commit is contained in:
Alexander Weidinger 2018-04-30 21:36:27 +02:00
parent 11366b3c17
commit 531ce6d3ad
6 changed files with 268 additions and 6 deletions

View File

@ -66,12 +66,15 @@ distribution.
Manages the side-wide ssh_known_hosts file and fills it with the
public SSH host keys of your minions (collected via the Salt mine)
and of hosts listed in you pillar data. You can restrict the set of minions
and of hosts listed in you pillar data. It's possible to include
minions managed via ``salt-ssh`` by using the ``known_hosts_salt_ssh`` renderer.
You can restrict the set of minions
whose keys are listed by using the pillar data ``openssh:known_hosts:target``
and ``openssh:known_hosts:tgt_type`` (those fields map directly to the
corresponding attributes of the ``mine.get`` function).
The Salt mine is used to share the public SSH host keys, you must thus
The **Salt mine** is used to share the public SSH host keys, you must thus
configure it accordingly on all hosts that must export their keys. Two
mine functions are required, one that exports the keys (one key per line,
as they are stored in ``/etc/ssh/ssh_host_*_key.pub``) and one that defines
@ -103,7 +106,54 @@ IPv6 behind one of those DNS entries matches an IPv4 or IPv6 behind the
official hostname of a minion, the alternate DNS name will be associated to the
minion's public SSH host key.
To add public keys of hosts not among your minions list them under the
To **include minions managed via salt-ssh** install the ``known_hosts_salt_ssh`` renderer::
# in pillar.top:
'*':
- openssh.known_hosts_salt_ssh
# In your salt/ directory:
# Link the pillar file:
mkdir pillar/openssh
ln -s ../../formulas/openssh-formula/_pillar/known_hosts_salt_ssh.sls pillar/openssh/known_hosts_salt_ssh.sls
Pillar ``openssh:known_hosts:salt_ssh`` overrides the Salt Mine.
The pillar is fed by a host key cache. Populate it by applying ``openssh.gather_host_keys``
to the salt master::
salt 'salt-master.example.test' state.apply openssh.gather_host_keys
The state tries to fetch the SSH host keys via ``salt-ssh``. It calls the command as user
``salt-master`` by default. The username can be changed via Pillar::
openssh:
known_hosts:
salt_ssh:
user: salt-master
You can use a cronjob to populate a host key cache::
# crontab -e -u salt-master
0 1 * * * salt 'salt-master.example.test' state.apply openssh.gather_host_keys
Or just add it to your salt master::
# states/top.sls:
base:
salt:
- openssh.known_hosts_salt_ssh
You can also use a "golden" known hosts file. It overrides the keys fetched by the cronjob.
This lets you re-use the trust estabished in the salt-ssh user's known_hosts file::
# In your salt/ directory: (Pillar expects the file here.)
ln -s /home/salt-master/.ssh/known_hosts ./known_hosts
# Test it:
salt-ssh 'minion' pillar.get 'openssh:known_hosts:salt_ssh'
To add **public keys of hosts not among your minions** list them under the
pillar key ``openssh:known_hosts:static``::
openssh:
@ -112,6 +162,7 @@ pillar key ``openssh:known_hosts:static``::
github.com: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq[...]'
gitlab.com: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA[...]'
Pillar ``openssh:known_hosts:static`` overrides ``openssh:known_hosts:salt_ssh``.
``openssh.moduli``
-----------------------

View File

@ -0,0 +1,113 @@
#!py
import logging as log
import os.path
import re
import subprocess
cache = {}
ssh_key_pattern = re.compile("^[^ ]+ (ssh-.+)$")
def config_dir():
if '__master_opts__' in __opts__:
# run started via salt-ssh
return __opts__['__master_opts__']['config_dir']
else:
# run started via salt
return __opts__['config_dir']
def cache_dir():
if '__master_opts__' in __opts__:
# run started via salt-ssh
return __opts__['__master_opts__']['cachedir']
else:
# run started via salt
return __opts__['cachedir']+'/../master'
def minions():
if not 'minions' in cache:
cache['minions'] = __salt__.slsutil.renderer(config_dir() + '/roster')
return cache['minions']
def host_variants(minion):
_variants = [minion]
def add_port_variant(host):
if 'port' in minions()[minion]:
_variants.append("[{}]:{}".format(host, minions()[minion]['port']))
add_port_variant(minion)
if 'host' in minions()[minion]:
host = minions()[minion]['host']
_variants.append(host)
add_port_variant(host)
return _variants
def host_keys_from_known_hosts(minion, path):
'''
Fetches all host keys of the given minion.
'''
if not os.path.isfile(path):
return []
pubkeys = []
def fill_pubkeys(host):
for line in host_key_of(host, path).splitlines():
match = ssh_key_pattern.search(line)
if match:
pubkeys.append(match.group(1))
# Try the minion ID and its variants first
for host in host_variants(minion):
fill_pubkeys(host)
# When no keys were found ...
if not pubkeys:
# ... fetch IP addresses via DNS and try them.
for host in (salt['dig.A'](minion) + salt['dig.AAAA'](minion)):
fill_pubkeys(host)
# When not a single key was found anywhere:
if not pubkeys:
log.error("No SSH host key found for {}. "
"You may need to add it to {}.".format(minion, path))
return "\n".join(pubkeys)
def host_key_of(host, path):
cmd = ["ssh-keygen", "-H", "-F", host, "-f", path]
call = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
out, err = call.communicate()
if err == '':
return out
else:
log.error("{} failed:\nSTDERR: {}\nSTDOUT: {}".format(
" ".join(cmd),
err,
out
))
return ""
def host_keys(minion_id):
# Get keys from trusted known_hosts file
trusted_keys = host_keys_from_known_hosts(minion_id,
config_dir()+'/known_hosts')
if trusted_keys:
print "trusted_keys"
return trusted_keys
# Get keys from host key cache
cache_file = "{}/known_hosts_salt_ssh/{}.pub".format(cache_dir(), minion_id)
try:
with open(cache_file, 'r') as f:
return f.read()
except IOError:
return ''
def run():
config = {
'public_ssh_host_keys': {},
'public_ssh_hostname': {}
}
for minion in minions().keys():
config['public_ssh_hostname'][minion] = minion
config['public_ssh_host_keys'][minion] = host_keys(minion)
return {'openssh': {'known_hosts': {'salt_ssh': config}}}
# vim: ts=4:sw=4:syntax=python

View File

@ -44,7 +44,7 @@
{%- endmacro -%}
{#- Pre-fetch pillar data #}
{%- set target = salt['pillar.get']('openssh:known_hosts:target', '*') -%}
{%- set target = salt['pillar.get']('openssh:known_hosts:target', "*.{}".format(grains['domain'])) -%}
{%- set tgt_type = salt['pillar.get']('openssh:known_hosts:tgt_type', 'glob') -%}
{%- set keys_function = salt['pillar.get']('openssh:known_hosts:mine_keys_function', 'public_ssh_host_keys') -%}
{%- set hostname_function = salt['pillar.get']('openssh:known_hosts:mine_hostname_function', 'public_ssh_hostname') -%}
@ -63,11 +63,33 @@
{%- endfor -%}
{%- endfor -%}
{#- Loop over targetted minions -#}
{#- Salt Mine #}
{%- set host_keys = salt['mine.get'](target, keys_function, tgt_type=tgt_type) -%}
{%- set host_names = salt['mine.get'](target, hostname_function, tgt_type=tgt_type) -%}
{#- Salt SSH (if any) #}
{%- for minion_id, minion_host_keys in salt['pillar.get'](
'openssh:known_hosts:salt_ssh:public_ssh_host_keys',
{}
).items() -%}
{%- if salt["match.{}".format(tgt_type)](target, minion_id=minion_id) -%}
{% do host_keys.update({minion_id: minion_host_keys}) %}
{%- endif -%}
{%- endfor -%}
{%- for minion_id, minion_host_name in salt['pillar.get'](
'openssh:known_hosts:salt_ssh:public_ssh_host_names',
{}
).items() -%}
{%- if salt["match.{}".format(tgt_type)](target, minion_id=minion_id) -%}
{% do host_names.update({minion_id: minion_host_name}) %}
{%- endif -%}
{%- endfor %}
{#- Static Pillar data #}
{%- do host_keys.update(salt['pillar.get']('openssh:known_hosts:static',
{}).items()) -%}
{#- Loop over targetted minions -#}
{%- for host, keys in host_keys| dictsort -%}
{{ known_host_entry(host, host_names, keys) }}
{%- endfor -%}

40
openssh/gather_host_keys Normal file
View File

@ -0,0 +1,40 @@
{%- set minions = salt.slsutil.renderer(opts['config_dir'] + '/roster') %}
{%- set cache_dir = opts['cachedir'] + '/known_hosts_salt_ssh' %}
{%- set cmd = "cat /etc/ssh/ssh_host_*_key.pub 2>/dev/null | sort" %}
{{ cache_dir }}:
file.directory:
- makedirs: True
{%- for minion_id in minions %}
{%- if loop.first %}
{%- set salt_ssh_cmd = "salt-ssh --out=json --static '{}' cmd.run_all '{}'".format(minion_id, cmd) %}
{%- set result = salt['cmd.run_all'](salt_ssh_cmd,
python_shell=True,
runas=salt['pillar.get']('openssh:known_hosts:salt_ssh:user', 'salt-master')
)
%}
{{ result }}
{%- set pubkeys = False %}
{%- if result[minion_id]['retcode'] == 0 %}
{% load_json as inner_result %}
{{ result[minion_id]['stdout'] }}
{%- endload %}
{%- set pubkeys = inner_result['stdout'] %}
{%- else %}
{%- do salt.log.error("{} failed: {}".format(salt_ssh_cmd, result)) %}
{%- endif %}
{%- if pubkeys %}
{{ cache_dir }}/{{ minion_id }}.pub:
file.managed:
- contents: |
{{ pubkeys | indent(8) }}
- require:
- file: {{ cache_dir }}
{%- endif %}
{%- endif %}
{%- endfor %}

View File

@ -0,0 +1,36 @@
{%- set minions = salt.slsutil.renderer(opts['config_dir'] + '/roster') %}
{%- set cache_dir = opts['cachedir'] + '/../master/known_hosts_salt_ssh' %}
{%- set cmd = "cat /etc/ssh/ssh_host_*_key.pub 2>/dev/null" %}
{{ cache_dir }}:
file.directory:
- makedirs: True
{%- for minion_id in minions %}
{%- set salt_ssh_cmd = "salt-ssh --out=json --static '{}' cmd.run_all '{}'".format(minion_id, cmd) %}
{%- set result = salt['cmd.run_all'](salt_ssh_cmd,
python_shell=True,
runas=salt['pillar.get']('openssh:known_hosts:salt_ssh:user', 'salt-master')
)
%}
{%- set pubkeys = False %}
{%- if result['retcode'] == 0 %}
{%- load_json as inner_result %}
{{ result['stdout'] }}
{%- endload %}
{%- set pubkeys = inner_result[minion_id]['stdout'].splitlines() | sort | join("\n") %}
{%- else %}
{%- do salt.log.error("{} failed: {}".format(salt_ssh_cmd, result)) %}
{%- endif %}
{%- if pubkeys %}
{{ cache_dir }}/{{ minion_id }}.pub:
file.managed:
- contents: |
{{ pubkeys | indent(8) }}
- require:
- file: {{ cache_dir }}
{%- endif %}
{%- endfor %}

View File

@ -303,7 +303,7 @@ openssh:
#hostnames:
# Restrict wich hosts you want to use via their hostname
# (i.e. ssh user@host instead of ssh user@host.example.com)
# target: '*' # Defaults to "*.{}".format(grains['domain']) with a fallback to '*'
# target: '*' # Defaults to "*.{{ grains['domain']}}"
# tgt_type: 'glob'
# To activate the defaults you can just set an empty dict.
#hostnames: {}