Support for appending a snippet to configuration file

This commit is contained in:
Jakub Jelen 2021-05-20 16:52:29 +02:00 committed by Jakub Jelen
parent fd22532d93
commit 380ebd21d9
10 changed files with 410 additions and 20 deletions

View file

@ -99,3 +99,9 @@ script:
ANSIBLE_FORCE_COLOR=1 ansible-playbook -i tests/inventory tests/tests_backup.yml --connection=local --become -v
&& (echo 'Backup test: pass' && exit 0)
|| (echo 'Backup test: fail' && exit 1)
# Test 13: Verify configuration append
- >
ANSIBLE_FORCE_COLOR=1 ansible-playbook -i tests/inventory tests/tests_namespace_append.yml --connection=local --become -v
&& (echo 'Append test: pass' && exit 0)
|| (echo 'Append test: fail' && exit 1)

View file

@ -156,6 +156,23 @@ if the system does not have hardware random number generator.
The path where the openssh configuration produced by this role should be saved.
This is useful mostly when generating configuration snippets to Include.
* `sshd_namespace_append`
By default (*null*), the role defines whole content of the configuration file
(with potential system defaults). To allow this role to be invoked from other
roles or from multiple places in a single playbok on systems that do not
support drop-in directory, we can define namespaces, that will allow us place
configuration snippets idempotently into a single configuration file. The only
requirement for these instances is to have different namespace name (this
variable). Other limitation of openssh configuration file such as that only the
first option specified in a configuration file is effective still apply.
Technically, the snippets are placed in `Match all` blocks (unless they contain
other match block) to make sure they are applied regardless the previous match
blocks. This allows to configure any non-conflicting options from different
roles invocations.
### Secondary role variables
These variables are used by the role internals and can be used to override the
@ -289,7 +306,7 @@ generated by the scripts in meta. New options should be added to the
`options_body` or `options_match`.
To regenerate the template, from within the meta/ directory run:
`./make_option_list >../templates/sshd_config.j2`
`./make_option_lists`
License
-------

View file

@ -65,6 +65,10 @@ sshd_hostkey_owner: root
sshd_hostkey_group: root
sshd_hostkey_mode: "0600"
# instead of replacing the whole configuration file, just append a specified
# snippet
sshd_namespace_append: null
### These variables are used by role internals and should not be used.
__sshd_defaults: {}
__sshd_os_supported: no

1
meta/01_ansible_head.j2 Normal file
View file

@ -0,0 +1 @@
# {{ ansible_managed }}

View file

@ -1,4 +1,3 @@
# {{ ansible_managed }}
{% macro render_option(key,value,indent=false) %}
{% if value is defined %}
{% if indent == true %} {% endif %}

View file

@ -1,16 +0,0 @@
#!/bin/sh
cat 10_top.j2
cat options_match |
awk '{
print "{{ render_option(\""$1"\",match[\""$1"\"],true) -}}"
}'
cat 20_middle.j2
cat options_body |
awk '{
print "{{ body_option(\""$1"\",sshd_"$1") -}}"
}'
cat 30_bottom.j2

41
meta/make_option_lists Executable file
View file

@ -0,0 +1,41 @@
#!/bin/sh
# Full configuration file
(
cat 01_ansible_head.j2
cat 10_top.j2
cat options_match |
awk '{
print "{{ render_option(\""$1"\",match[\""$1"\"],true) -}}"
}'
cat 20_middle.j2
cat options_body |
awk '{
print "{{ body_option(\""$1"\",sshd_"$1") -}}"
}'
cat 30_bottom.j2
) >../templates/sshd_config.j2
# Snippet of configuration file
(
cat 10_top.j2 |
sed -e "s/indent=false/indent=true/"
cat options_match |
awk '{
print "{{ render_option(\""$1"\",match[\""$1"\"],true) -}}"
}'
cat 20_middle.j2
cat options_body |
awk '{
print "{{ body_option(\""$1"\",sshd_"$1") -}}"
}'
cat 30_bottom.j2
) >../templates/sshd_config_snippet.j2

View file

@ -101,9 +101,9 @@
when:
- __sshd_runtime_directory | d(false) | bool
- name: Create the configuration file
- name: Create the complete configuration file
template:
src: sshd_config.j2
src: sshd_config_snippet.j2
dest: "{{ sshd_config_file }}"
owner: "{{ sshd_config_owner }}"
group: "{{ sshd_config_group }}"
@ -116,6 +116,29 @@
{% endif %}
backup: "{{ sshd_backup }}"
notify: reload_sshd
when: sshd_namespace_append is none
- name: Update configuration file snippet
blockinfile:
path: "{{ sshd_config_file }}"
owner: "{{ sshd_config_owner }}"
group: "{{ sshd_config_group }}"
mode: "{{ sshd_config_mode }}"
block: |
Match all
{{ lookup('template', 'sshd_config_snippet.j2') }}
create: yes
marker: "# {mark} sshd system role managed block: namespace {{ sshd_namespace_append }}"
validate: >-
{% if sshd_test_hostkey is defined and sshd_test_hostkey.path is defined %}
{{ sshd_binary }} -t -f %s -h {{ sshd_test_hostkey.path }}/rsa_key
{% else %}
{{ sshd_binary }} -t -f %s
{% endif %}
backup: "{{ sshd_backup }}"
notify: reload_sshd
when: sshd_namespace_append is not none
rescue:
- name: re-raise the error
fail:

View file

@ -0,0 +1,241 @@
{% macro render_option(key,value,indent=true) %}
{% if value is defined %}
{% if indent == true %} {% endif %}
{% if value is sameas true %}
{{ key }} yes
{% elif value is sameas false %}
{{ key }} no
{% elif value is string or value is number %}
{{ key }} {{ value }}
{% else %}
{% for i in value %}
{{ key }} {{ i }}
{% endfor %}
{% endif %}
{% endif %}
{% endmacro %}
{% macro body_option(key,override) %}
{% set value = undefined %}
{% if override is defined %}
{% set value = override %}
{% elif sshd[key] is defined %}
{% set value = sshd[key] %}
{% elif __sshd_defaults[key] is defined and sshd_skip_defaults != true %}
{% set value = __sshd_defaults[key] %}
{% endif %}
{{ render_option(key,value) -}}
{% endmacro %}
{% macro match_block(match_list) %}
{% if match_list["Condition"] is defined %}
{% set match_list = [ match_list ]%}
{% endif %}
{% if match_list is iterable %}
{% for match in match_list %}
Match {{ match["Condition"] }}
{{ render_option("AcceptEnv",match["AcceptEnv"],true) -}}
{{ render_option("AllowAgentForwarding",match["AllowAgentForwarding"],true) -}}
{{ render_option("AllowGroups",match["AllowGroups"],true) -}}
{{ render_option("AllowStreamLocalForwarding",match["AllowStreamLocalForwarding"],true) -}}
{{ render_option("AllowTcpForwarding",match["AllowTcpForwarding"],true) -}}
{{ render_option("AllowUsers",match["AllowUsers"],true) -}}
{{ render_option("AuthenticationMethods",match["AuthenticationMethods"],true) -}}
{{ render_option("AuthorizedKeysCommand",match["AuthorizedKeysCommand"],true) -}}
{{ render_option("AuthorizedKeysCommandUser",match["AuthorizedKeysCommandUser"],true) -}}
{{ render_option("AuthorizedKeysFile",match["AuthorizedKeysFile"],true) -}}
{{ render_option("AuthorizedPrincipalsCommand",match["AuthorizedPrincipalsCommand"],true) -}}
{{ render_option("AuthorizedPrincipalsCommandUser",match["AuthorizedPrincipalsCommandUser"],true) -}}
{{ render_option("AuthorizedPrincipalsFile",match["AuthorizedPrincipalsFile"],true) -}}
{{ render_option("Banner",match["Banner"],true) -}}
{{ render_option("ChrootDirectory",match["ChrootDirectory"],true) -}}
{{ render_option("ClientAliveCountMax",match["ClientAliveCountMax"],true) -}}
{{ render_option("ClientAliveInterval",match["ClientAliveInterval"],true) -}}
{{ render_option("DenyGroups",match["DenyGroups"],true) -}}
{{ render_option("DenyUsers",match["DenyUsers"],true) -}}
{{ render_option("ForceCommand",match["ForceCommand"],true) -}}
{{ render_option("GatewayPorts",match["GatewayPorts"],true) -}}
{{ render_option("GSSAPIAuthentication",match["GSSAPIAuthentication"],true) -}}
{{ render_option("HostbasedAcceptedKeyTypes",match["HostbasedAcceptedKeyTypes"],true) -}}
{{ render_option("HostbasedAuthentication",match["HostbasedAuthentication"],true) -}}
{{ render_option("HostbasedUsesNameFromPacketOnly",match["HostbasedUsesNameFromPacketOnly"],true) -}}
{{ render_option("Include",match["Include"],true) -}}
{{ render_option("IPQoS",match["IPQoS"],true) -}}
{{ render_option("KbdInteractiveAuthentication",match["KbdInteractiveAuthentication"],true) -}}
{{ render_option("KerberosAuthentication",match["KerberosAuthentication"],true) -}}
{{ render_option("LogLevel",match["LogLevel"],true) -}}
{{ render_option("MaxAuthTries",match["MaxAuthTries"],true) -}}
{{ render_option("MaxSessions",match["MaxSessions"],true) -}}
{{ render_option("PasswordAuthentication",match["PasswordAuthentication"],true) -}}
{{ render_option("PermitEmptyPasswords",match["PermitEmptyPasswords"],true) -}}
{{ render_option("PermitListen",match["PermitListen"],true) -}}
{{ render_option("PermitOpen",match["PermitOpen"],true) -}}
{{ render_option("PermitRootLogin",match["PermitRootLogin"],true) -}}
{{ render_option("PermitTTY",match["PermitTTY"],true) -}}
{{ render_option("PermitTunnel",match["PermitTunnel"],true) -}}
{{ render_option("PermitUserRC",match["PermitUserRC"],true) -}}
{{ render_option("PubkeyAcceptedKeyTypes",match["PubkeyAcceptedKeyTypes"],true) -}}
{{ render_option("PubkeyAuthentication",match["PubkeyAuthentication"],true) -}}
{{ render_option("RDomain",match["RDomain"],true) -}}
{{ render_option("RekeyLimit",match["RekeyLimit"],true) -}}
{{ render_option("RevokedKeys",match["RevokedKeys"],true) -}}
{{ render_option("RhostsRSAAuthentication",match["RhostsRSAAuthentication"],true) -}}
{{ render_option("RSAAuthentication",match["RSAAuthentication"],true) -}}
{{ render_option("SetEnv",match["SetEnv"],true) -}}
{{ render_option("StreamLocalBindMask",match["StreamLocalBindMask"],true) -}}
{{ render_option("StreamLocalBindUnlink",match["StreamLocalBindUnlink"],true) -}}
{{ render_option("TrustedUserCAKeys",match["TrustedUserCAKeys"],true) -}}
{{ render_option("X11DisplayOffset",match["X11DisplayOffset"],true) -}}
{{ render_option("X11MaxDisplays",match["X11MaxDisplays"],true) -}}
{{ render_option("X11Forwarding",match["X11Forwarding"],true) -}}
{{ render_option("X11UseLocalHost",match["X11UseLocalHost"],true) -}}
{% endfor %}
{% endif %}
{% endmacro %}
{% macro match_iterate_block(match_list) %}
{% if match_list | type_debug == "list" %}
{% for match in match_list %}
{{ match_block(match) -}}
{% endfor %}
{% else %}
{{ match_block(match_list) -}}
{% endif %}
{% endmacro %}
{{ body_option("Port",sshd_Port) -}}
{{ body_option("AddressFamily",sshd_AddressFamily) -}}
{{ body_option("ListenAddress",sshd_ListenAddress) -}}
{{ body_option("Protocol",sshd_Protocol) -}}
{{ body_option("HostKey",sshd_HostKey) -}}
{{ body_option("AcceptEnv",sshd_AcceptEnv) -}}
{{ body_option("AllowAgentForwarding",sshd_AllowAgentForwarding) -}}
{{ body_option("AllowGroups",sshd_AllowGroups) -}}
{{ body_option("AllowStreamLocalForwarding",sshd_AllowStreamLocalForwarding) -}}
{{ body_option("AllowTcpForwarding",sshd_AllowTcpForwarding) -}}
{{ body_option("AllowUsers",sshd_AllowUsers) -}}
{{ body_option("AuthenticationMethods",sshd_AuthenticationMethods) -}}
{{ body_option("AuthorizedKeysCommand",sshd_AuthorizedKeysCommand) -}}
{{ body_option("AuthorizedKeysCommandUser",sshd_AuthorizedKeysCommandUser) -}}
{{ body_option("AuthorizedKeysFile",sshd_AuthorizedKeysFile) -}}
{{ body_option("AuthorizedPrincipalsCommand",sshd_AuthorizedPrincipalsCommand) -}}
{{ body_option("AuthorizedPrincipalsCommandUser",sshd_AuthorizedPrincipalsCommandUser) -}}
{{ body_option("AuthorizedPrincipalsFile",sshd_AuthorizedPrincipalsFile) -}}
{{ body_option("Banner",sshd_Banner) -}}
{{ body_option("CASignatureAlgorithms",sshd_CASignatureAlgorithms) -}}
{{ body_option("ChallengeResponseAuthentication",sshd_ChallengeResponseAuthentication) -}}
{{ body_option("ChrootDirectory",sshd_ChrootDirectory) -}}
{{ body_option("Ciphers",sshd_Ciphers) -}}
{{ body_option("ClientAliveCountMax",sshd_ClientAliveCountMax) -}}
{{ body_option("ClientAliveInterval",sshd_ClientAliveInterval) -}}
{{ body_option("Compression",sshd_Compression) -}}
{{ body_option("DebianBanner",sshd_DebianBanner) -}}
{{ body_option("DenyGroups",sshd_DenyGroups) -}}
{{ body_option("DenyUsers",sshd_DenyUsers) -}}
{{ body_option("DisableForwarding",sshd_DisableForwarding) -}}
{{ body_option("ExposeAuthInfo",sshd_ExposeAuthInfo) -}}
{{ body_option("FingerprintHash",sshd_FingerprintHash) -}}
{{ body_option("ForceCommand",sshd_ForceCommand) -}}
{{ body_option("GatewayPorts",sshd_GatewayPorts) -}}
{{ body_option("GSSAPIAuthentication",sshd_GSSAPIAuthentication) -}}
{{ body_option("GSSAPICleanupCredentials",sshd_GSSAPICleanupCredentials) -}}
{{ body_option("GSSAPIKeyExchange",sshd_GSSAPIKeyExchange) -}}
{{ body_option("GSSAPIKexAlgorithms",sshd_GSSAPIKexAlgorithms) -}}
{{ body_option("GSSAPIStoreCredentialsOnRekey",sshd_GSSAPIStoreCredentialsOnRekey) -}}
{{ body_option("GSSAPIStrictAcceptorCheck",sshd_GSSAPIStrictAcceptorCheck) -}}
{{ body_option("HPNBufferSize",sshd_HPNBufferSize) -}}
{{ body_option("HPNDisabled",sshd_HPNDisabled) -}}
{{ body_option("HostCertificate",sshd_HostCertificate) -}}
{{ body_option("HostKeyAgent",sshd_HostKeyAgent) -}}
{{ body_option("HostKeyAlgorithms",sshd_HostKeyAlgorithms) -}}
{{ body_option("HostbasedAcceptedKeyTypes",sshd_HostbasedAcceptedKeyTypes) -}}
{{ body_option("HostbasedAuthentication",sshd_HostbasedAuthentication) -}}
{{ body_option("HostbasedUsesNameFromPacketOnly",sshd_HostbasedUsesNameFromPacketOnly) -}}
{{ body_option("Include",sshd_Include) -}}
{{ body_option("IPQoS",sshd_IPQoS) -}}
{{ body_option("IgnoreRhosts",sshd_IgnoreRhosts) -}}
{{ body_option("IgnoreUserKnownHosts",sshd_IgnoreUserKnownHosts) -}}
{{ body_option("KbdInteractiveAuthentication",sshd_KbdInteractiveAuthentication) -}}
{{ body_option("KerberosAuthentication",sshd_KerberosAuthentication) -}}
{{ body_option("KerberosGetAFSToken",sshd_KerberosGetAFSToken) -}}
{{ body_option("KerberosOrLocalPasswd",sshd_KerberosOrLocalPasswd) -}}
{{ body_option("KerberosTicketCleanup",sshd_KerberosTicketCleanup) -}}
{{ body_option("KexAlgorithms",sshd_KexAlgorithms) -}}
{{ body_option("KeyRegenerationInterval",sshd_KeyRegenerationInterval) -}}
{{ body_option("LogLevel",sshd_LogLevel) -}}
{{ body_option("LoginGraceTime",sshd_LoginGraceTime) -}}
{{ body_option("MACs",sshd_MACs) -}}
{{ body_option("MaxAuthTries",sshd_MaxAuthTries) -}}
{{ body_option("MaxSessions",sshd_MaxSessions) -}}
{{ body_option("MaxStartups",sshd_MaxStartups) -}}
{{ body_option("NoneEnabled",sshd_NoneEnabled) -}}
{{ body_option("PasswordAuthentication",sshd_PasswordAuthentication) -}}
{{ body_option("PermitEmptyPasswords",sshd_PermitEmptyPasswords) -}}
{{ body_option("PermitListen",sshd_PermitListen) -}}
{{ body_option("PermitOpen",sshd_PermitOpen) -}}
{{ body_option("PermitRootLogin",sshd_PermitRootLogin) -}}
{{ body_option("PermitTTY",sshd_PermitTTY) -}}
{{ body_option("PermitTunnel",sshd_PermitTunnel) -}}
{{ body_option("PermitUserEnvironment",sshd_PermitUserEnvironment) -}}
{{ body_option("PermitUserRC",sshd_PermitUserRC) -}}
{{ body_option("PidFile",sshd_PidFile) -}}
{{ body_option("PrintLastLog",sshd_PrintLastLog) -}}
{{ body_option("PrintMotd",sshd_PrintMotd) -}}
{{ body_option("PubkeyAcceptedKeyTypes",sshd_PubkeyAcceptedKeyTypes) -}}
{{ body_option("PubkeyAuthOptions",sshd_PubkeyAuthOptions) -}}
{{ body_option("PubkeyAuthentication",sshd_PubkeyAuthentication) -}}
{{ body_option("RSAAuthentication",sshd_RSAAuthentication) -}}
{{ body_option("RekeyLimit",sshd_RekeyLimit) -}}
{{ body_option("RevokedKeys",sshd_RevokedKeys) -}}
{{ body_option("RDomain",sshd_RDomain) -}}
{{ body_option("RhostsRSAAuthentication",sshd_RhostsRSAAuthentication) -}}
{{ body_option("SecurityKeyProvider",sshd_SecurityKeyProvider) -}}
{{ body_option("SetEnv",sshd_SetEnv) -}}
{{ body_option("ServerKeyBits",sshd_ServerKeyBits) -}}
{{ body_option("StreamLocalBindMask",sshd_StreamLocalBindMask) -}}
{{ body_option("StreamLocalBindUnlink",sshd_StreamLocalBindUnlink) -}}
{{ body_option("StrictModes",sshd_StrictModes) -}}
{{ body_option("Subsystem",sshd_Subsystem) -}}
{{ body_option("SyslogFacility",sshd_SyslogFacility) -}}
{{ body_option("TCPKeepAlive",sshd_TCPKeepAlive) -}}
{{ body_option("TcpRcvBufPoll",sshd_TcpRcvBufPoll) -}}
{{ body_option("TrustedUserCAKeys",sshd_TrustedUserCAKeys) -}}
{{ body_option("UseDNS",sshd_UseDNS) -}}
{{ body_option("UseLogin",sshd_UseLogin) -}}
{{ body_option("UsePAM",sshd_UsePAM) -}}
{{ body_option("UsePrivilegeSeparation",sshd_UsePrivilegeSeparation) -}}
{{ body_option("VersionAddendum",sshd_VersionAddendum) -}}
{{ body_option("X11DisplayOffset",sshd_X11DisplayOffset) -}}
{{ body_option("X11MaxDisplays",sshd_X11MaxDisplays) -}}
{{ body_option("X11Forwarding",sshd_X11Forwarding) -}}
{{ body_option("X11UseLocalhost",sshd_X11UseLocalhost) -}}
{{ body_option("XAuthLocation",sshd_XAuthLocation) -}}
{% if sshd['Match'] is defined %}
{{ match_iterate_block(sshd['Match']) -}}
{% endif %}
{% if sshd_match is defined %}
{{ match_iterate_block(sshd_match) -}}
{% endif %}
{% if sshd_match_1 is defined %}
{{ match_block(sshd_match_1) -}}
{% endif %}
{% if sshd_match_2 is defined %}
{{ match_block(sshd_match_2) -}}
{% endif %}
{% if sshd_match_3 is defined %}
{{ match_block(sshd_match_3) -}}
{% endif %}
{% if sshd_match_4 is defined %}
{{ match_block(sshd_match_4) -}}
{% endif %}
{% if sshd_match_5 is defined %}
{{ match_block(sshd_match_5) -}}
{% endif %}
{% if sshd_match_6 is defined %}
{{ match_block(sshd_match_6) -}}
{% endif %}
{% if sshd_match_7 is defined %}
{{ match_block(sshd_match_7) -}}
{% endif %}
{% if sshd_match_8 is defined %}
{{ match_block(sshd_match_8) -}}
{% endif %}
{% if sshd_match_9 is defined %}
{{ match_block(sshd_match_9) -}}
{% endif %}

View file

@ -0,0 +1,74 @@
---
- hosts: all
vars:
__sshd_test_backup_files:
- /etc/ssh/sshd_config
tasks:
- name: "Backup configuration files"
include_tasks: tasks/backup.yml
- name: Append configuration block to default configuration file
include_role:
name: ansible-sshd
vars:
sshd_config_file: /etc/ssh/sshd_config
sshd_skip_defaults: true
sshd_namespace_append: nm1
sshd:
AcceptEnv: EDITOR
PasswordAuthentication: yes
Match:
Condition: user root
AllowAgentForwarding: no
- name: Append second configuration block to default configuration file
include_role:
name: ansible-sshd
vars:
sshd_config_file: /etc/ssh/sshd_config
sshd_skip_defaults: true
sshd_namespace_append: nm2
sshd:
AcceptEnv: LS_COLORS
PasswordAuthentication: no
Match:
Condition: Address 127.0.0.1
AllowTcpForwarding: no
- name: Verify the options are correctly set
block:
- meta: flush_handlers
- name: Print current configuration file
slurp:
src: /etc/ssh/sshd_config
register: config
- name: List effective configuration using sshd -T
command: sshd -T -Cuser=root,host=localhost,addr=127.0.0.1,
register: runtime
- name: Check content of configuration file
assert:
that:
- "'AcceptEnv EDITOR' in config.content | b64decode"
- "'PasswordAuthentication yes' in config.content | b64decode"
- "'Match user root' in config.content | b64decode"
- "'AllowAgentForwarding no' in config.content | b64decode"
- "'AcceptEnv LS_COLORS' in config.content | b64decode"
- "'PasswordAuthentication no' in config.content | b64decode"
- "'Match Address 127.0.0.1' in config.content | b64decode"
- "'AllowTcpForwarding no' in config.content | b64decode"
- name: Check the configuration values are effective
# note, the options are in lower-case here
assert:
that:
- "'acceptenv EDITOR' in runtime.stdout"
- "'acceptenv LS_COLORS' in runtime.stdout"
- "'passwordauthentication yes' in runtime.stdout"
tags: tests::verify
- name: "Restore configuration files"
include_tasks: tasks/restore.yml