diff --git a/.travis.yml b/.travis.yml index dc7852d..e7870f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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) diff --git a/README.md b/README.md index fff839c..14c333e 100644 --- a/README.md +++ b/README.md @@ -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 ------- diff --git a/defaults/main.yml b/defaults/main.yml index 81a9919..233e224 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -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 diff --git a/meta/01_ansible_head.j2 b/meta/01_ansible_head.j2 new file mode 100644 index 0000000..e2bb153 --- /dev/null +++ b/meta/01_ansible_head.j2 @@ -0,0 +1 @@ +# {{ ansible_managed }} diff --git a/meta/10_top.j2 b/meta/10_top.j2 index 040437b..c2cadd8 100644 --- a/meta/10_top.j2 +++ b/meta/10_top.j2 @@ -1,4 +1,3 @@ -# {{ ansible_managed }} {% macro render_option(key,value,indent=false) %} {% if value is defined %} {% if indent == true %} {% endif %} diff --git a/meta/make_option_list b/meta/make_option_list deleted file mode 100755 index b555093..0000000 --- a/meta/make_option_list +++ /dev/null @@ -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 diff --git a/meta/make_option_lists b/meta/make_option_lists new file mode 100755 index 0000000..ae15b2c --- /dev/null +++ b/meta/make_option_lists @@ -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 diff --git a/tasks/install.yml b/tasks/install.yml index f5023d2..1b10ff8 100644 --- a/tasks/install.yml +++ b/tasks/install.yml @@ -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: diff --git a/templates/sshd_config_snippet.j2 b/templates/sshd_config_snippet.j2 new file mode 100644 index 0000000..5852ad4 --- /dev/null +++ b/templates/sshd_config_snippet.j2 @@ -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 %} diff --git a/tests/tests_namespace_append.yml b/tests/tests_namespace_append.yml new file mode 100644 index 0000000..92f327d --- /dev/null +++ b/tests/tests_namespace_append.yml @@ -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