diff --git a/cdist/conf/type/__sshd_config/explorer/state b/cdist/conf/type/__sshd_config/explorer/state
new file mode 100644
index 00000000..75c68b8a
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/explorer/state
@@ -0,0 +1,121 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist 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.
+#
+# cdist 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 cdist. If not, see .
+#
+# Determines the current state of the config option.
+# Possible output:
+# - present: "should" option present in config file
+# - default: the "should" option is the default -> don’t know if present
+# - absent: no such option present in config file
+#
+
+joinlines() { sed -n -e H -e "\${x;s/^\\n//;s/\\n/${1:?}/g;p;}"; }
+trlower() { tr '[:upper:]' '[:lower:]'; }
+tolower() { printf '%s' "$*" | trlower; }
+
+default_value() {
+ sshd -T -f /dev/null -C "$(make_conn_spec)" \
+ | sed -n -e 's/^'"$(tolower "${1:?}")"'[[:blank:]]\{1,\}//p'
+}
+
+make_conn_spec() {
+ if test -s "${__object:?}/parameter/match"
+ then
+ _match_file="${__object:?}/parameter/match"
+ else
+ _match_file='/dev/null'
+ fi
+
+ for _kw in \
+ addr=Address \
+ user=User \
+ host=Host \
+ laddr=LocalAddress \
+ lport=LocalPort \
+ rdomain=RDomain
+ do
+ _specname=${_kw%%=*}
+ _confname=$(tolower "${_kw#*=}")
+ while read -r _k _v
+ do
+ if test "$(tolower "${_k}")" = "${_confname}"
+ then
+ printf '%s=%s\n' "${_specname}" "${_v}"
+ continue 2
+ fi
+ done <"${_match_file}"
+
+ # NOTE: Print test spec even for empty keys to suppress errors like:
+ # 'Match User' in configuration but 'user' not in connection test specification.
+ # except lport:
+ # Invalid port '' in test mode specification lport=
+ test "${_specname}" = 'lport' || printf '%s=\n' "${_specname}"
+ done \
+ | joinlines ','
+ unset _match_file
+}
+
+sshd_config_file=$(cat "${__object:?}/parameter/file")
+state_should=$(cat "${__object:?}/parameter/state")
+
+if test -s "${__object:?}/parameter/option"
+then
+ option_name=$(cat "${__object:?}/parameter/option")
+else
+ option_name=${__object_id:?}
+fi
+
+value_should=$(cat "${__object:?}/parameter/value" 2>/dev/null) \
+|| test "${state_should}" = absent || exit 0 # param optional if --state absent
+
+command -v sshd >/dev/null 2>&1 || {
+ echo 'Cannot find sshd.' >&2
+ exit 1
+}
+
+test -e "${sshd_config_file}" || {
+ echo 'absent'
+ exit 0
+}
+
+value_is=$(
+ sshd -T -f "${sshd_config_file}" -C "$(make_conn_spec)" \
+ | sed -n -e 's/^'"$(tolower "${option_name}")"'[[:blank:]]\{1,\}//p')
+
+if printf '%s\n' "${value_is}" | {
+ if test -n "${value_should}"
+ then
+ grep -q -x -F "${value_should}"
+ else
+ # if no value provided, assume "any" value
+ grep -q -e .
+ fi
+ }
+then
+ if default_value "${option_name}" | grep -q -x -F "${value_is}"
+ then
+ # Might produce false positives for default values.
+ # TODO: Manual checking should be done, but for simplicity, this case is
+ # currently ignored here.
+ echo default
+ else
+ echo present
+ fi
+else
+ echo absent
+fi
diff --git a/cdist/conf/type/__sshd_config/files/update_sshd_config.awk b/cdist/conf/type/__sshd_config/files/update_sshd_config.awk
new file mode 100644
index 00000000..d0bc2b4b
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/files/update_sshd_config.awk
@@ -0,0 +1,293 @@
+# -*- mode: awk; indent-tabs-mode: t -*-
+
+function usage() {
+ print_err("Usage: awk -f update_sshd_config.awk -- -o set|unset [-m 'User git'] -l 'X11Forwarding no' /etc/ssh/sshd_config")
+}
+
+function print_err(s) { print s | "cat >&2" }
+
+function alength(a, i) {
+ for (i = 0; (i + 1) in a; ++i);
+ return i
+}
+
+function join(sep, a, i, s) {
+ for (i = i ? i : 1; i in a; i++)
+ s = s sep a[i]
+ return substr(s, 2)
+}
+
+function getopt(opts, argv, target, files, i, c, lv, idx, nf) {
+ # trivial getopt(3) implementation; only basic functionality
+ if (argv[1] == "--") i++
+ for (i += 1; i in argv; i++) {
+ if (lv) { target[c] = argv[i]; lv = 0; continue }
+ if (argv[i] ~ /^-/) {
+ c = substr(argv[i], 2, 1)
+ idx = index(opts, c)
+ if (!idx) {
+ print_err(sprintf("invalid option -%c\n", c))
+ continue
+ }
+ if (substr(opts, idx + 1, 1) == ":") {
+ # option takes argument
+ if (length(argv[i]) > 2)
+ target[c] = substr(argv[i], 3)
+ else
+ lv = 1
+ } else {
+ target[c] = 1
+ }
+ } else
+ files[++nf] = argv[i]
+ }
+}
+
+# tokenise configuration line
+# this function mimics the counterpart in OpenSSH (misc.c)
+# but it returns two (next token SUBSEP rest) because I didn’t want to have to
+# simulate any pointer magic.
+function strdelim_internal(s, split_equals, old) {
+ if (!s)
+ return ""
+
+ old = s
+
+ if (!match(s, WHITESPACE "|" QUOTE "" (split_equals ? "|" EQUALS : "")))
+ return s
+
+ s = substr(s, RSTART)
+ old = substr(old, 1, RSTART - 1)
+
+ if (s ~ "^" QUOTE) {
+ old = substr(old, 2)
+
+ # Find matching quote
+ if (match(s, QUOTE)) {
+ old = substr(old, 1, RSTART)
+ # s = substr()
+ if (match(s, "^" WHITESPACE "*"))
+ s = substr(s, RLENGTH)
+ return old
+ } else {
+ # no matching quote
+ return ""
+ }
+ }
+
+ if (match(s, "^" WHITESPACE "+")) {
+ sub("^" WHITESPACE "+", "", s)
+ if (split_equals)
+ sub(EQUALS WHITESPACE "*", "", s)
+ } else if (s ~ "^" EQUALS) {
+ s = substr(s, 2)
+ }
+
+ return old SUBSEP s
+}
+function strdelim(s) { return strdelim_internal(s, 1) }
+function strdelimw(s) { return strdelim_internal(s, 0) }
+
+function singleton_option(opt) {
+ return tolower(opt) !~ /^(acceptenv|allowgroups|allowusers|authenticationmethods|authorizedkeysfile|denygroups|denyusers|hostcertificate|hostkey|listenaddress|logverbose|permitlisten|permitopen|port|setenv|subsystem)$/
+}
+
+function print_update() {
+ if (mode) {
+ if (match_only) printf "\t"
+ printf "%s\n", line_should
+ updated = 1
+ }
+}
+
+BEGIN {
+ FS = "\n" # disable field splitting
+
+ WHITESPACE = "[ \t]" # servconf.c, misc.c:strdelim_internal (without line breaks, cf. bugs)
+ QUOTE = "[\"]" # misc.c:strdelim_internal
+ EQUALS = "[=]"
+
+ split("", opts)
+ split("", files)
+ getopt("ho:l:m:", ARGV, opts, files)
+
+ if (opts["h"]) { usage(); exit (e="0") }
+
+ line_should = opts["l"]
+ match_only = opts["m"]
+ num_files = alength(files)
+
+ if (num_files != 1 || !opts["o"] || !line_should) {
+ usage()
+ exit (e=126)
+ }
+
+ if (opts["o"] == "set") {
+ mode = 1
+ } else if (opts["o"] == "unset") {
+ mode = 0
+ } else {
+ print_err(sprintf("invalid mode %s\n", mode))
+ exit (e=1)
+ }
+
+ if (mode) {
+ # loop over sshd_config twice!
+ ARGV[2] = ARGV[1] = files[1]
+ ARGC = 3
+ } else {
+ # only loop once
+ ARGV[1] = files[1]
+ ARGC = 2
+ }
+
+ split(strdelim(line_should), should, SUBSEP)
+ option_should = tolower(should[1])
+ value_should = should[2]
+}
+
+{
+ line = $0
+
+ # Strip trailing whitespace. Allow \f (form feed) at EOL only
+ sub("(" WHITESPACE "|\f)*$", "", line)
+
+ # Strip leading whitespace
+ sub("^" WHITESPACE "*", "", line)
+
+ if (match(line, "^#" WHITESPACE "*")) {
+ prefix = substr(line, RSTART, RLENGTH)
+ line = substr(line, RSTART + RLENGTH)
+ } else {
+ prefix = ""
+ }
+
+ line_type = "invalid"
+ option_is = value_is = ""
+
+ if (line) {
+ split(strdelim(line), toks, SUBSEP)
+
+ if (tolower(toks[1]) == "match") {
+ MATCH = (prefix ~ /^#/ ? "#" : "") join(" ", toks, 2)
+ line_type = "match"
+ } else if (toks[1] ~ /^[A-Za-z][A-Za-z0-9]+$/) {
+ # This could be an option line
+ line_type = "option"
+ option_is = tolower(toks[1])
+ value_is = toks[2]
+ }
+ } else {
+ line_type = "empty"
+ }
+}
+
+# mode: unset
+
+!mode {
+ # delete matching config
+ if (prefix !~ /^#/)
+ if (MATCH == match_only && option_is == option_should)
+ if (!value_should || value_should == value_is)
+ next
+
+ print
+ next
+}
+
+
+# mode: set
+
+mode && NR == FNR {
+ if (line_type == "option") {
+ if (MATCH !~ /^#/) {
+ if (prefix ~ /^#/) {
+ # comment line
+ last_occ[MATCH, "#" option_is] = FNR
+ } else {
+ # option line
+ last_occ[MATCH, option_is] = FNR
+ }
+ last_occ[MATCH] = FNR
+ }
+ } else if (line_type == "invalid" && !prefix) {
+ # INVALID LINE
+ print_err(sprintf("%s: syntax error on line %u\n", ARGV[0], FNR))
+ }
+
+ next
+}
+
+# before second pass prepare hashes containing location information to be used
+# in the second pass.
+mode && NR > FNR && FNR == 1 {
+ # First we drop the locations of commented-out options if a non-commented
+ # option is available. If a non-commented option is available, we will
+ # append new config options there to have them all at one place.
+ for (k in last_occ) {
+ if (k ~ /^#/) {
+ # delete entries of commented out match blocks
+ delete last_occ[k]
+ continue
+ }
+
+ split(k, parts, SUBSEP)
+
+ if (parts[2] ~ /^#/ && ((parts[1], substr(parts[2], 2)) in last_occ))
+ delete last_occ[k]
+ }
+
+ # Reverse the option => line mapping. The line_map allows for easier lookups
+ # in the second pass.
+ # We only keep options, not top-level keywords, because we can only have
+ # one entry per line and there are conflicts with last lines of "sections".
+ for (k in last_occ) {
+ if (!index(k, SUBSEP)) continue
+ line_map[last_occ[k]] = k
+ }
+}
+
+# Second pass
+mode && line_map[FNR] == match_only SUBSEP option_should && !updated {
+ split(line_map[FNR], parts, SUBSEP)
+
+ # If option allows multiple values, print current value
+ if (!singleton_option(parts[2])) {
+ if (value_should != value_is)
+ print
+ }
+
+ print_update()
+
+ next
+}
+
+mode { print }
+
+# Is a comment option
+mode && line_map[FNR] == match_only SUBSEP "#" option_should && !updated {
+ print_update()
+}
+
+# Last line of the should match section
+mode && last_occ[match_only] == FNR && !updated {
+ # NOTE: Inserting empty lines is only cosmetic. It is only done if
+ # different options are next to each other and not in a match block
+ # (match blocks are usually not in the default config and thus don’t
+ # contain commented blocks.)
+ if (line && option_is != option_should && !MATCH)
+ print ""
+ print_update()
+}
+
+END {
+ if (e) exit e
+
+ if (mode && !updated) {
+ if (match_only && MATCH != match_only) {
+ printf "\nMatch %s\n", match_only
+ }
+
+ print_update()
+ }
+}
diff --git a/cdist/conf/type/__sshd_config/gencode-remote b/cdist/conf/type/__sshd_config/gencode-remote
new file mode 100755
index 00000000..0b44dfa7
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/gencode-remote
@@ -0,0 +1,97 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist 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.
+#
+# cdist 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 cdist. If not, see .
+#
+
+joinlines() { sed -n -e H -e "\${x;s/^\\n//;s/\\n/${1:?}/g;p;}"; }
+
+state_is=$(cat "${__object:?}/explorer/state")
+state_should=$(cat "${__object:?}/parameter/state")
+
+if test "${state_is}" = "${state_should}" -o "${state_is}" = 'default'
+then
+ # nothing to do (if the value is the default, ignore its state)
+ exit 0
+fi
+
+case ${state_should}
+in
+ (present)
+ mode='set'
+ ;;
+ (absent)
+ mode='unset'
+ ;;
+ (*)
+ printf 'Invalid --state: %s\n' "${state_should}" >&2
+ exit 1
+ ;;
+esac
+
+sshd_config_file=$(cat "${__object:?}/parameter/file")
+
+quote() { printf "'%s'" "$(printf '%s' "$*" | sed -e "s/'/'\\\\''/g")"; }
+drop_awk_comments() { quote "$(sed '/^[[:blank:]]*#.*$/d;/^$/d' "$@")"; }
+
+# Ensure the sshd_config file is there
+cat <$(quote "${sshd_config_file}")
+ chown 0:0 $(quote "${sshd_config_file}")
+ chmod 0644 $(quote "${sshd_config_file}")
+}
+
+EOF
+
+match_only=
+if test -s "${__object:?}/parameter/match"
+then
+ match_only=$(joinlines ' ' <"${__object:?}/parameter/match")
+fi
+
+if test -s "${__object:?}/parameter/option"
+then
+ option_line=$(cat "${__object:?}/parameter/option")
+else
+ option_line=${__object_id:?}
+fi
+
+if test -s "${__object:?}/parameter/value"
+then
+ option_line="${option_line} $(cat "${__object:?}/parameter/value")"
+fi
+
+# Send message on config update
+printf '%s%s %s\n' "${mode}" "${match_only:+ [${match_only}]}" \
+ "${option_line}" >>"${__messages_out:?}"
+
+# Update sshd_config (remote code)
+cat <$(quote "${sshd_config_file}.tmp") \\
+|| exit
+
+cmp -s $(quote "${sshd_config_file}") $(quote "${sshd_config_file}.tmp") || {
+ sshd -t -f $(quote "${sshd_config_file}.tmp") \\
+ && cat $(quote "${sshd_config_file}.tmp") >$(quote "${sshd_config_file}")
+}
+rm -f $(quote "${sshd_config_file}.tmp")
+EOF
diff --git a/cdist/conf/type/__sshd_config/man.rst b/cdist/conf/type/__sshd_config/man.rst
new file mode 100644
index 00000000..8b0069ac
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/man.rst
@@ -0,0 +1,94 @@
+cdist-type__sshd_config(7)
+==========================
+
+NAME
+----
+cdist-type__sshd_config - Manage options in sshd_config
+
+
+DESCRIPTION
+-----------
+This space intentionally left blank.
+
+
+REQUIRED PARAMETERS
+-------------------
+None.
+
+
+OPTIONAL PARAMETERS
+-------------------
+file
+ The path to the sshd_config file to edit.
+ Defaults to ``/etc/ssh/sshd_config``.
+match
+ Restrict this option to apply only for certain connections.
+ Allowed values are what would be allowed to be written after a ``Match``
+ keyword in ``sshd_config``, e.g. ``--match 'User anoncvs'``.
+
+ Can be used multiple times. All of the values are ANDed together.
+option
+ The name of the option to manipulate. Defaults to ``__object_id``.
+state
+ Can be:
+
+ - ``present``: ensure a matching config line is present (or the default
+ value).
+ - ``absent``: ensure no matching config line is present.
+value
+ The option's value to be assigned to the option (if ``--state present``) or
+ removed (if ``--state absent``).
+
+ This option is required if ``--state present``. If not specified and
+ ``--state absent``, all values for the given option are removed.
+
+
+BOOLEAN PARAMETERS
+------------------
+None.
+
+
+EXAMPLES
+--------
+
+.. code-block:: sh
+
+ # Disallow root logins with password
+ __sshd_config PermitRootLogin --value without-password
+
+ # Disallow password-based authentication
+ __sshd_config PasswordAuthentication --value no
+
+ # Accept the EDITOR environment variable
+ __sshd_config AcceptEnv:EDITOR --option AcceptEnv --value EDITOR
+
+ # Force command for connections as git user
+ __sshd_config git@ForceCommand --match 'User git' --option ForceCommand \
+ --value 'cd ~git && exec git-shell ${SSH_ORIGINAL_COMMAND:+-c "${SSH_ORIGINAL_COMMAND}"}'
+
+
+SEE ALSO
+--------
+:strong:`sshd_config`\ (5)
+
+
+BUGS
+----
+- This type assumes a nicely formatted config file,
+ i.e. no config options spanning multiple lines.
+- ``Include`` directives are ignored.
+- Config options are not added/removed to/from the config file if their value is
+ the default value.
+
+
+AUTHORS
+-------
+Dennis Camera
+
+
+COPYING
+-------
+Copyright \(C) 2020 Dennis Camera. 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.
diff --git a/cdist/conf/type/__sshd_config/manifest b/cdist/conf/type/__sshd_config/manifest
new file mode 100755
index 00000000..566bde90
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/manifest
@@ -0,0 +1,48 @@
+#!/bin/sh -e
+#
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
+#
+# This file is part of cdist.
+#
+# cdist 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.
+#
+# cdist 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 cdist. If not, see .
+#
+
+os=$(cat "${__global:?}/explorer/os")
+
+state_should=$(cat "${__object:?}/parameter/state")
+
+case ${os}
+in
+ (alpine|centos|fedora|redhat|scientific|debian|devuan|ubuntu)
+ if test "${state_should}" != 'absent'
+ then
+ __package openssh-server --state present
+ fi
+ ;;
+ (archlinux|gentoo|slackware|suse)
+ if test "${state_should}" != 'absent'
+ then
+ __package openssh --state present
+ fi
+ ;;
+ (freebsd|netbsd|openbsd)
+ # whitelist
+ ;;
+ (*)
+ printf 'Your operating system (%s) is currently not supported by this type (%s)\n' \
+ "${os}" "${__type##*/}" >&2
+ printf 'Please contribute an implementation for it if you can.\n' >&2
+ exit 1
+ ;;
+esac
diff --git a/cdist/conf/type/__sshd_config/parameter/default/file b/cdist/conf/type/__sshd_config/parameter/default/file
new file mode 100644
index 00000000..d8ea5dfc
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/parameter/default/file
@@ -0,0 +1 @@
+/etc/ssh/sshd_config
diff --git a/cdist/conf/type/__sshd_config/parameter/default/state b/cdist/conf/type/__sshd_config/parameter/default/state
new file mode 100644
index 00000000..e7f6134f
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/parameter/default/state
@@ -0,0 +1 @@
+present
diff --git a/cdist/conf/type/__sshd_config/parameter/optional b/cdist/conf/type/__sshd_config/parameter/optional
new file mode 100644
index 00000000..922ab093
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/parameter/optional
@@ -0,0 +1,4 @@
+file
+option
+state
+value
diff --git a/cdist/conf/type/__sshd_config/parameter/optional_multiple b/cdist/conf/type/__sshd_config/parameter/optional_multiple
new file mode 100644
index 00000000..02b1d1a9
--- /dev/null
+++ b/cdist/conf/type/__sshd_config/parameter/optional_multiple
@@ -0,0 +1 @@
+match