From: martin f. krafft <madduck@madduck.net>
Date: Fri, 19 Dec 2014 12:17:05 +0000 (+0100)
Subject: Initial checkin
X-Git-Url: https://git.madduck.net/code/gsc.git/commitdiff_plain/13f2a361642e48bebba84a1a4986e9b0d41bdcc6?ds=inline

Initial checkin
---

13f2a361642e48bebba84a1a4986e9b0d41bdcc6
diff --git a/README b/README
new file mode 100644
index 0000000..5b4d286
--- /dev/null
+++ b/README
@@ -0,0 +1,148 @@
+gsc — Git service configuration
+-------------------------------
+2014-12-19-1306 madduck@madduck.net
+
+The following describes a method for the configuration of services, using Git
+as a backend. A service in this context is e.g. a mail server like Postfix or
+a DNS server like NSD, which runs on a Unix system and is configured using
+files usually situated in the /etc directory. When those files are changed,
+usually some steps have to be taken to fix up permissions, compile/merge
+files, and reload the service.
+
+These steps — editing configuration files and reloading services — are also
+the domain of so-called configuration management systems, such as CFengine and
+Puppet. While these are indispensable tools for the general management of
+machines, it is often the case that the fine-grained configuration of
+a complex mailserver requires more work to express in terms of the
+configuration management syntax than there are benefits.
+
+gsc is an alternative way of doing things. In a nutshell:
+
+  - configuration is maintained in Git and something like gitolite can be used
+    to manage fine-grained access control;
+
+  - upon push, a repo-side hook runs, reads from the repository a number of
+    hosts to poke and proceeds to do just that using SSH forced commands;
+
+  - a dedicated user on each target machine receives the poke, updates the
+    local Git repository and executes an in-tree Makefile, which is
+    responsible for the integration of the changes into the running service,
+    possibly using in-tree host-specific parameters.
+
+A word for the security-conscious
+'''''''''''''''''''''''''''''''''
+It is true that one might question the use of in-tree configuration files and
+the Makefile. After all, it allows anyone with write-access to the Git repo to
+impersonate the dedicated poke user, though not without leaving a trace.
+
+In the end it's a matter of trust and if you're ready to assume that someone
+who's supposed to be able to configure a given service on a machine can't
+really do any harm by impersonating the user designed to configure the given
+service, then you're on the safe side.
+
+The benefit of this approach is that it avoids operation as root whenever
+possible, defering to sudo for just those steps that require root privileges.
+
+Set-up by example: nsd
+----------------------
+
+Let's walk through a simple example and configure the nsd DNS server to be
+configurable/manageable with gsc.
+
+  0. We'll start with the assumption that the contents of /etc/nsd is already
+     tracked in a central Git repository and that trusted DNS admins can push
+     there.
+
+     We'll also assume that changes to the nsd config should be pushed to two
+     DNS servers, grimble.example.org and grumble.example.org.
+
+  1. Create a new, password-less (!) SSH keyfile using ssh-keygen on your
+     local machine (which needs a clone of the above Git repo), store the key
+     into .gsc/poke-key[.pub] inside that repo, and edit the public keyfile by
+     prepending the following to the first (and only) line (broken here into
+     two lines for readability):
+
+       command="make -sC target git_pull update",no-agent-forwarding,
+       no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding
+
+     You can also add a from="" limit as per the sshd(8) manpage, although
+     there are benefits in not doing that, namely the ability for anyone with
+     access to the repo to poke the servers.
+
+     Commit the two files .gsc/poke-key and .gsc/poke-key.pub to the
+     repository.
+
+  2. On the two "poke" hosts, make sure you have Git, make and sudo installed.
+     Then add a new system user, e.g. nsd_update (with group nsd_update,
+     disabled password, home directory /var/local/nsd_update, and shell
+     /bin/sh).
+
+  3. Copy .gsc/poke-key.pub to ~nsd_update/.ssh/authorized_keys and make sure
+     to run `chown -R nsd_update ~nsd_update/.ssh` or similar.
+
+  4. Clone the Git repo with --shared=group to /etc/nsd (or /etc/nsd3) and
+     similarly chown it, so that the poke user can maintain it without root
+     rights. Consequentially, the user "nsd" needs to be able to read
+     (possibly even write) the files in this repository, so you need to either
+     add nsd to the group nsd_update and ensure g+rwX permissions (using the
+     Makefile, see below), or employ ACLs.
+
+     Make sure that the nsd_update user can pull from the repo, either by
+     making it publicly available, or by setting up a new password-less SSH
+     key for nsd_update on each host, which is then given access to the
+     central repo.
+
+  5. Create a symlink ~nsd_update/target to /etc/nsd (or whatever directory
+     you chose). This was done to avoid having to make /etc/nsd the
+     home directory of the update user and not hardcoding the location of the
+     configuration.
+
+  6. Now you should be able to poke a host from your workstation:
+
+       ssh -Ti .gsc/poke-key -o UserKnownHostFile=.gsc/ssh_known_hosts \
+         nsd_update@grimble.example.org
+
+     which should print something like
+
+       make: *** No rule to make target 'git_pull'.  Stop.
+
+     Do this for all hosts in pokehosts to populate .gsc/ssh_known_hosts
+     (verifying the fingerprints, of course), and then commit this file as
+     well.
+
+     As we are using forced commands, it makes no difference what you put
+     after the user@host argument in the above. However, we may need this in
+     the future, so let's reserve it. Please refrain from using it and/or let
+     me know if you have any ideas.
+
+  7. Install the gsc-post-receive hook to hooks/post-receive in the central
+     git repo. When using gitolite, I suggest the use of per-repository hooks
+     (see further down).
+
+  8. Commit a file .gsc/pokehosts to the repository, listing each target
+     server one-per-line in user@host style, e.g.
+
+       nsd_update@grimble.example.org
+       nsd_update@grumble.example.org
+
+     The rest of each line is unused for now, but reserved for future use, so
+     don't put anything else there.
+
+  9. Now push your commits to the central repo, which should cause the hook to
+     poke both hosts, yielding the make error.
+
+ 10. Now all that's left to do is write /etc/nsd/Makefile with at least the
+     targets git_pull and update, which should run git-pull (possibly with
+     --rebase) and then cause the appropriate next steps. Use sudo sparingly
+     for maximum security benefit.
+
+     Please refer to the examples/ directory in the gsc repository for
+     a simple example that works with nsd, and which also handles per-host
+     difference by parametrising e.g. IP addresses or commands needed to
+     reload/restart the service.
+
+TODO:
+
+  - per-host branches
+  - how to recover from faulty pushes, e.g. if the server was taken down
+  - gitolite notes
diff --git a/examples/nsd/Makefile b/examples/nsd/Makefile
new file mode 100644
index 0000000..10cfec7
--- /dev/null
+++ b/examples/nsd/Makefile
@@ -0,0 +1,42 @@
+#!/usr/bin/make -f
+#
+# Makefile — gsc example for nsd
+#
+# An example Makefile usable to manage nsd in the context of gsc.
+#
+# Copyright © 2014 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+
+HOSTNAME = $(shell hostname --fqdn)
+PARAMS_FILE = $(wildcard ./params_$(HOSTNAME))
+
+ifeq ($(PARAMS_FILE),)
+$(error No parameters file found for host $(HOSTNAME))
+endif
+
+.PHONY: update
+update: .sentinel fixperms
+.sentinel: .git/index Makefile nsd.conf zones.conf
+	@. $(PARAMS_FILE) \
+	&& echo "$$NSD_REBUILD_COMMAND" && $$NSD_REBUILD_COMMAND \
+	&& echo "$$NSD_RESTART_COMMAND" && $$NSD_RESTART_COMMAND
+	@touch $@
+
+fixperms: ALWAYS
+	chmod -R o= .
+	setfacl -R -m group:nsd_update:rX,default:group:nsd_update:rX .
+	setfacl -R -m default:mask::rX,default:group::rX .
+	setfacl -R -m user:nsd:rX,default:user:nsd:rX .
+.PHONY: fixperms
+
+git_pull:
+	git pull
+
+ALWAYS:
+%.conf: %.conf-generator ALWAYS
+	@rm -f .$@.tmp && umask 0337 && ./$< $(PARAMS_FILE) > .$@.tmp
+	@[ -e $@ ] || touch $@
+	@cmp --quiet .$@.tmp $@ || \
+		{ chmod 600 $@ && mv .$@.tmp $@ && echo $@ updated. ; }
+	@rm -f .$@.tmp
diff --git a/examples/nsd/nsd.conf-generator b/examples/nsd/nsd.conf-generator
new file mode 100755
index 0000000..5efc395
--- /dev/null
+++ b/examples/nsd/nsd.conf-generator
@@ -0,0 +1,36 @@
+#!/bin/sh
+#
+# nsd.conf-generator — generate nsd.conf through interpolation
+#
+# Copyright © 2014 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+set -eu
+
+PARAMS_FILE="$1";
+if [ ! -r "$PARAMS_FILE" ]; then
+	echo >&2 "E: cannot read parameters from $PARAMS_FILE"
+	exit 1
+fi
+. $PARAMS_FILE
+
+NSD_DIR=${0%/*}
+[ "$NSD_DIR" = "." ] && NSD_DIR=${PWD}
+
+exec cat <<_eof
+server:
+	# uncomment to specify specific interfaces to bind (default all).
+	ip-address: 127.0.1.1
+	ip-address: $BIND_IPV4
+	ip-address: $BIND_IPV6
+
+	# don't answer VERSION.BIND and VERSION.SERVER CHAOS class queries
+	hide-version: yes
+
+	# identify the server (CH TXT ID.SERVER entry).
+	identity: "$IDENTITY"
+
+# do not include subdir yet (cf. #772776)
+#include: "$NSD_DIR/nsd.conf.d/*.conf"
+include: "$NSD_DIR/zones.conf"
+_eof
diff --git a/examples/nsd/params_grimple.example.org b/examples/nsd/params_grimple.example.org
new file mode 100644
index 0000000..c076503
--- /dev/null
+++ b/examples/nsd/params_grimple.example.org
@@ -0,0 +1,5 @@
+BIND_IPV4=192.0.2.13
+BIND_IPV6=2001:db8:1::53:0
+IDENTITY=grimble.example.org
+NSD_REBUILD_COMMAND=":"
+NSD_RESTART_COMMAND="sudo systemctl restart nsd.service"
diff --git a/examples/nsd/params_grumble.example.org b/examples/nsd/params_grumble.example.org
new file mode 100644
index 0000000..e4c42f8
--- /dev/null
+++ b/examples/nsd/params_grumble.example.org
@@ -0,0 +1,5 @@
+BIND_IPV4=192.0.2.105
+BIND_IPV6=2001:db8:ff::53:0
+IDENTITY=grumble.example.org
+NSD_REBUILD_COMMAND="sudo nsdc rebuild"
+NSD_RESTART_COMMAND="sudo invoke-rc.d nsd3 restart"
diff --git a/examples/nsd/zones.conf-generator b/examples/nsd/zones.conf-generator
new file mode 100755
index 0000000..3829073
--- /dev/null
+++ b/examples/nsd/zones.conf-generator
@@ -0,0 +1,19 @@
+#!/bin/sh
+#
+# zones.conf-generator — generate zones.conf through interpolation
+#
+# Copyright © 2014 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+set -eu
+
+for zonefile in zones/*.zone arpazones/*.arpa; do
+  zone=${zonefile%.zone}; zone=${zone##*/};
+  printf 'zone:\n\tname: "%s"\n\tzonefile: "%s"\n' $zone $zonefile;
+done
+
+cat <<_eof
+
+## checksums:
+_eof
+find arpazones zones -type f -exec sha256sum {} + | sed -e 's,^,#,'
diff --git a/gsc-post-receive b/gsc-post-receive
new file mode 100755
index 0000000..ff07fde
--- /dev/null
+++ b/gsc-post-receive
@@ -0,0 +1,38 @@
+#!/bin/sh
+#
+# gsc-post-receive — Git post-receive hook to do gsc poking
+#
+# Copyright © 2014 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+set -eu
+
+TEMPDIR=$(mktemp -d gsc.XXXXXXXXXX)
+TRAPSIGNALS="0 1 2 3 4 5 6 7 8 10 11 12 13 14 15"
+cleanup() { rm -rf "$TEMPDIR"; trap - $TRAPSIGNALS; }
+trap cleanup $TRAPSIGNALS
+
+if ! git show HEAD:.gsc/pokehosts 2>/dev/null > $TEMPDIR/pokehosts; then
+  exit 0
+fi
+git show HEAD:.gsc/poke-key > $TEMPDIR/poke-key
+git show HEAD:.gsc/ssh_known_hosts > $TEMPDIR/ssh_known_hosts
+
+poke_host() {
+  echo >&2 "*** Poking $1..."
+
+  ssh -T -o ConnectTimeout=10 \
+    -o ForwardAgent=no -o ForwardX11=no \
+    -o PreferredAuthentications=publickey \
+    -o IdentitiesOnly=yes \
+    -o IdentityFile="$TEMPDIR/poke-key" \
+    -o StrictHostKeyChecking=yes \
+    -o UserKnownHostsFile=$TEMPDIR/ssh_known_hosts \
+    $2 $1 </dev/null | sed 's,^,    ,' >&2
+
+  echo >&2 "*** Done poking $1."
+}
+
+while read target ssh_opts; do
+  poke_host $target $ssh_opts
+done < $TEMPDIR/pokehosts