0

I'm writing a DNS-01 extension to the widely used Let's Encrypt acme.sh, that uses a locally running DNS server such as Bind, Unbound, Knot DNS, etc, rather than an API to an external service provider, for those running a local or remote daemon without nsupdate/RFC2136 access.

Being widely used, it needs to be able to operate on almost any *nix platform. I've done what I can to write it securely. In particular, the bulk of the code is platform indifferent (standard sh only) and code related to the DNS daemon is isolated in a single shell dispatcher function, called like this:

_daemon_service_call 'RELOAD CONFIG'
_daemon_service_call 'CHECK_DAEMON_RUNNING'

with the code behind it being roughly:

_demon_service_call () {
case "$daemon_name" in
   Unbound) _daemon_action_unbound "$1";;
   Bind) _daemon_action_bind "$1";;
   *) return 1 ;;
esac
}

_daemon_action_unbound() {
case "$1" in
   RELOAD_CONFIG) echo "$2" > cat "${tmpfile}"
               /usr/sbin/unbound-control -c "${tmpfile}" ;;
   CHECK_DAEMON_RUNNING)
               /usr/sbin/unbound-control status ;;
   *) return 1;;
esac
}

_daemon_action_bind() {
case "$1" in
   RELOAD_CONFIG) echo "$2" > cat "${tmpfile}"
               ......
   CHECK_DAEMON_RUNNING)
              .....
   *) return 1 ;;
esac
}

and so on. The aim being to minimise daemon-dependent code, and make it easy to add handlers for other daemons. So far so good. Scope for malicious usage not on the horizon, once the user names the daemon to call.

The problem is that the daemons themselves can be installed in a variety of ways. For example, on FreeBSD the daemon might be installed and accessed directly ("/usr/bin/unbound-checkconf", "usr/bin/unbound-control") but might in other cases need to be called via /usr/sbin/chroot -u USER:GROUP / /usr/bin/unbound-checkconf, or calls to iocage/warden entrypoints, or a completely different command to access unbound-checkconf on Fedora or Redhat or Illumos or DragonflyBSD, or whatever. On OPNsense the code shouldn't call unbound-control at all, it should call the internal backend API (configctl dns configure or similar) to call it instead.

Once I let the user specify more than a daemon name - once they can enter the relevant command for their platform as part of the module config - there's scope for that text to be maliciously targeted or set. If it can be used in the format daemon_reload_config_cmd="/usr/sbin/unbound-control -c %s", then what about a malicious setting of daemon_reload_config_cmd="cp /etc/passwd /var/http-root-dir/;#" or daemon_reload_config_cmd="echo 'stuff'>/etc/sshd.conf ", or any number of other exploit codes, or calls to paths that the user controls?

I could have the code drop to an unprivileged level beforehand (if acme runs privileged), but that also differs by platform and most of these calls need privilege to manage the DNS daemon. I'm not sure how, or whether, that's a solution.

As I understand it, ACME stores sensitive user-provided data like the DNS provider credentials in a safe way, on its first run, and uses the stored copies for future runs. But there's a bit of a difference between storing service logon credentials (however sensitive DNS may be), and storing part of a CLI command that will be implicitly trusted, and used to build the path to an executable, in a command line that will be run privileged on future occasions.

At a pinch, I could require the user to manually create a file with chmod 0644 + chown root, containing the commands to use in these calls (there's only about 5 of them and they're only one-liner templates so it's easy), which would ensure trustworthiness, but that breaks the acme model which aims to keep it easy for the user and pass most things in variables AFAIK.

Is there any good way to do this that's valid cross platform, or is it inherently doomed and unsafe?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.