From 825f928d824f728088606bcbf112d30d7a76f627 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Wed, 21 Apr 2021 08:16:40 +0200 Subject: [PATCH] tests: add very basic end-to-end test with real ssh server At the moment only the helper upload without any actual configuration is tested. --- .builds/archlinux.yml | 2 +- .builds/freebsd.yml | 2 +- .builds/openbsd.yml | 2 +- .gitignore | 6 + .gitlab-ci.yml | 8 +- Makefile | 7 + cmd/safcm/config/config.go | 1 + cmd/safcm/main_sync_test.go | 153 ++++++++++++++++++ cmd/safcm/sync.go | 5 +- cmd/safcm/testdata/ssh/prepare.sh | 24 +++ cmd/safcm/testdata/ssh/project/groups.yaml | 0 cmd/safcm/testdata/ssh/project/hosts.yaml | 1 + cmd/safcm/testdata/ssh/ssh/ssh_config | 7 + cmd/safcm/testdata/ssh/sshd/sshd_config | 16 ++ .../testdata/ssh/sshd/sshd_config.openbsd | 15 ++ rpc/conn.go | 3 +- rpc/dial.go | 20 ++- 17 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 cmd/safcm/main_sync_test.go create mode 100755 cmd/safcm/testdata/ssh/prepare.sh create mode 100644 cmd/safcm/testdata/ssh/project/groups.yaml create mode 100644 cmd/safcm/testdata/ssh/project/hosts.yaml create mode 100644 cmd/safcm/testdata/ssh/ssh/ssh_config create mode 100644 cmd/safcm/testdata/ssh/sshd/sshd_config create mode 100644 cmd/safcm/testdata/ssh/sshd/sshd_config.openbsd diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index d8ebea7..4adc85f 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -6,5 +6,5 @@ tasks: cd safcm ./ci/run # Also run all tests as root - sudo chown -Rh root:root . + sudo chown -Rh root:root .. sudo ./ci/run diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index e5dba6d..e3ea472 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -8,5 +8,5 @@ tasks: cd safcm ./ci/run # Also run all tests as root - sudo chown -Rh root:wheel . + sudo chown -Rh root:wheel .. sudo ./ci/run diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index 7f1d40c..025d49a 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -16,6 +16,6 @@ tasks: # Go does not yet support -race on OpenBSD ./ci/run GOFLAGS= # Also run all tests as root (-R and -h conflict on OpenBSD) - doas chown -R root:wheel . + doas chown -R root:wheel .. # Remove $HOME hack once Go 1.16 is available doas /bin/sh -c "HOME=$HOME ./ci/run GOFLAGS=" diff --git a/.gitignore b/.gitignore index 0b4325c..37a596e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +/cmd/safcm/testdata/ssh/ssh/authorized_keys +/cmd/safcm/testdata/ssh/ssh/id_ed25519 +/cmd/safcm/testdata/ssh/ssh/id_ed25519.pub +/cmd/safcm/testdata/ssh/ssh/known_hosts +/cmd/safcm/testdata/ssh/sshd/ssh_host_key +/cmd/safcm/testdata/ssh/sshd/ssh_host_key.pub /remote/helpers/ /safcm /tags diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 10f9811..e2ec0a5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,13 +1,19 @@ .template-docker: &template-docker before_script: - apt-get update - - apt-get install --no-install-recommends --yes build-essential ca-certificates git golang golang-1.16 golang-golang-x-tools make + - apt-get install --no-install-recommends --yes build-essential ca-certificates git golang golang-1.16 golang-golang-x-tools make openssh-server script: + # Gitlab-runner uses umask 0000 (wtf?!) and mixes nobody and root user + # when setting up the environment. This breaks ssh's permission check on + # authorized_keys. + - chown -R root:root /builds + - chmod -R go-w /builds # NOTE: golang is still using golang-1.15 - mkdir -p $HOME/go/bin - ln -sf /usr/lib/go-1.16/bin/go $HOME/go/bin - ln -sf /usr/lib/go-1.16/bin/gofmt $HOME/go/bin # + - mkdir /run/sshd - ./ci/run debian-sid: diff --git a/Makefile b/Makefile index f4cc011..5549539 100644 --- a/Makefile +++ b/Makefile @@ -16,11 +16,18 @@ safcm: cd cmd/safcm/testdata/project && ../../../../safcm fixperms 2> /dev/null test: + ./cmd/safcm/testdata/ssh/prepare.sh go vet ./... go test $(GOFLAGS) ./... clean: rm -rf remote/helpers/ rm -f safcm + rm -f cmd/safcm/testdata/ssh/ssh/authorized_keys + rm -f cmd/safcm/testdata/ssh/ssh/id_ed25519 + rm -f cmd/safcm/testdata/ssh/ssh/id_ed25519.pub + rm -f cmd/safcm/testdata/ssh/ssh/known_hosts + rm -f cmd/safcm/testdata/ssh/sshd/ssh_host_key + rm -f cmd/safcm/testdata/ssh/sshd/ssh_host_key.pub .PHONY: all test clean safcm diff --git a/cmd/safcm/config/config.go b/cmd/safcm/config/config.go index bae137f..fc01c8d 100644 --- a/cmd/safcm/config/config.go +++ b/cmd/safcm/config/config.go @@ -30,6 +30,7 @@ type Config struct { DryRun bool `yaml:"-"` // set via command line Quiet bool `yaml:"-"` // set via command line LogLevel safcm.LogLevel `yaml:"-"` // set via command line + SshConfig string `yaml:"-"` // set via command line DetectGroups []string `yaml:"detect_groups"` GroupOrder []string `yaml:"group_order"` diff --git a/cmd/safcm/main_sync_test.go b/cmd/safcm/main_sync_test.go new file mode 100644 index 0000000..8d702db --- /dev/null +++ b/cmd/safcm/main_sync_test.go @@ -0,0 +1,153 @@ +// Copyright (C) 2021 Simon Ruderich +// +// This program 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. +// +// This program 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 this program. If not, see . + +package main_test + +import ( + "fmt" + "net" + "os" + "os/exec" + "regexp" + "runtime" + "strings" + "testing" + "time" + + "ruderich.org/simon/safcm/testutil" +) + +func TestSyncSshEndToEnd(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(cwd) + + var suffix string + // Needs different options in sshd_config + if runtime.GOOS == "openbsd" { + suffix = ".openbsd" + } + + sshDir := cwd + "/testdata/ssh" + sshCmd := exec.Command("/usr/sbin/sshd", + "-D", // stay in foreground + "-e", // write messages to stderr instead of syslog + "-f", sshDir+"/sshd/sshd_config"+suffix, + "-h", sshDir+"/sshd/ssh_host_key", + "-o", "AuthorizedKeysFile="+sshDir+"/ssh/authorized_keys", + ) + sshCmd.Stderr = os.Stderr + err = sshCmd.Start() + if err != nil { + t.Fatal(err) + } + defer sshCmd.Process.Kill() + + // Wait until SSH server is ready (up to 30 seconds) + for i := 0; i < 30; i++ { + conn, err := net.Dial("tcp", "127.0.0.1:29327") + if err == nil { + conn.Close() + break + } + time.Sleep(time.Second) + } + + err = os.Chdir(sshDir + "/project") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + remove bool + args []string + exp string + expErr error + }{ + + { + "no settings", + true, + []string{"no-settings.example.org"}, + `[info] [no-settings.example.org] remote helper upload in progress +`, + nil, + }, + { + "no settings (no helper upload)", + false, + []string{"no-settings.example.org"}, + ``, + nil, + }, + { + "no settings (verbose)", + true, + []string{"-log", "verbose", "no-settings.example.org"}, + `[info] [no-settings.example.org] remote helper upload in progress +[verbose] [no-settings.example.org] host groups: all no-settings.example.org +[verbose] [no-settings.example.org] host group priorities (desc. order): no-settings.example.org +`, + nil, + }, + { + "no settings (debug2)", + true, + []string{"-log", "debug2", "no-settings.example.org"}, + `[info] [no-settings.example.org] remote helper upload in progress +[verbose] [no-settings.example.org] host groups: all no-settings.example.org +[verbose] [no-settings.example.org] host group priorities (desc. order): no-settings.example.org +`, + nil, + }, + } + + remotePath := fmt.Sprintf("/tmp/safcm-remote-%d", os.Getuid()) + + logRegexp := regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `) + detectedRegexp := regexp.MustCompile(`detected_\S+`) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.remove { + os.Remove(remotePath) + } + + args := append([]string{"sync", + "-sshconfig", sshDir + "/ssh/ssh_config", + }, tc.args...) + cmd := exec.Command("../../../../../safcm", args...) + out, err := cmd.CombinedOutput() + + var tmp []string + for _, x := range strings.Split(string(out), "\n") { + // Strip parts which change on each run (LOG) + // or depending on the system (DET) + x = logRegexp.ReplaceAllString(x, "") + x = detectedRegexp.ReplaceAllString(x, "") + tmp = append(tmp, x) + } + res := strings.Join(tmp, "\n") + + testutil.AssertEqual(t, "res", res, tc.exp) + testutil.AssertErrorEqual(t, "err", err, tc.expErr) + }) + } + + os.Remove(remotePath) +} diff --git a/cmd/safcm/sync.go b/cmd/safcm/sync.go index 238ed62..98b118e 100644 --- a/cmd/safcm/sync.go +++ b/cmd/safcm/sync.go @@ -76,6 +76,8 @@ func MainSync(args []string) error { "hide successful, non-trigger commands with no output from host changes listing") optionLog := flag.String("log", "info", "set log `level`; "+ "levels: error, info, verbose, debug, debug2, debug3") + optionSshConfig := flag.String("sshconfig", "", + "`path` to ssh configuration file; used for tests") flag.CommandLine.Parse(args[2:]) @@ -110,6 +112,7 @@ func MainSync(args []string) error { cfg.DryRun = *optionDryRun cfg.Quiet = *optionQuiet cfg.LogLevel = level + cfg.SshConfig = *optionSshConfig toSync, err := hostsToSync(names, allHosts, allGroups) if err != nil { @@ -364,7 +367,7 @@ func (s *Sync) Host(wg *sync.WaitGroup) error { }() // Connect to remote host - err := conn.DialSSH(s.host.SshUser, s.host.Name) + err := conn.DialSSH(s.host.SshUser, s.host.Name, s.config.SshConfig) if err != nil { return err } diff --git a/cmd/safcm/testdata/ssh/prepare.sh b/cmd/safcm/testdata/ssh/prepare.sh new file mode 100755 index 0000000..6608a3a --- /dev/null +++ b/cmd/safcm/testdata/ssh/prepare.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Create files for SSH server + +set -eu + + +cd "$(dirname "$0")" + +# Generate new keys so nobody else can login as this user. Host keys would be +# safe to commit but lets not needlessly commit private keys anyway. +if ! test -e sshd/ssh_host_key; then + ssh-keygen -q -t ed25519 -N '' -f sshd/ssh_host_key +fi +if ! test -e ssh/id_ed25519; then + ssh-keygen -q -t ed25519 -N '' -f ssh/id_ed25519 +fi +if ! test -e ssh/authorized_keys; then + cat ssh/id_ed25519.pub > ssh/authorized_keys +fi +if ! test -e ssh/known_hosts; then + printf '[127.0.0.1]:29327 ' > ssh/known_hosts + cat sshd/ssh_host_key.pub >> ssh/known_hosts +fi diff --git a/cmd/safcm/testdata/ssh/project/groups.yaml b/cmd/safcm/testdata/ssh/project/groups.yaml new file mode 100644 index 0000000..e69de29 diff --git a/cmd/safcm/testdata/ssh/project/hosts.yaml b/cmd/safcm/testdata/ssh/project/hosts.yaml new file mode 100644 index 0000000..b6a0dce --- /dev/null +++ b/cmd/safcm/testdata/ssh/project/hosts.yaml @@ -0,0 +1 @@ +- name: no-settings.example.org diff --git a/cmd/safcm/testdata/ssh/ssh/ssh_config b/cmd/safcm/testdata/ssh/ssh/ssh_config new file mode 100644 index 0000000..77f6fd5 --- /dev/null +++ b/cmd/safcm/testdata/ssh/ssh/ssh_config @@ -0,0 +1,7 @@ +IdentityFile ../ssh/id_ed25519 +IdentitiesOnly yes +UserKnownHostsFile ../ssh/known_hosts + +Host no-settings.example.org + Hostname 127.0.0.1 + Port 29327 diff --git a/cmd/safcm/testdata/ssh/sshd/sshd_config b/cmd/safcm/testdata/ssh/sshd/sshd_config new file mode 100644 index 0000000..d43eb49 --- /dev/null +++ b/cmd/safcm/testdata/ssh/sshd/sshd_config @@ -0,0 +1,16 @@ +Port 29327 +ListenAddress 127.0.0.1 +PidFile none +LogLevel ERROR + +# Only permit pubkey authentication +PubkeyAuthentication yes +PermitRootLogin prohibit-password +# +ChallengeResponseAuthentication no +GSSAPIAuthentication no +HostbasedAuthentication no +KbdInteractiveAuthentication no +KerberosAuthentication no +PasswordAuthentication no +UsePAM no diff --git a/cmd/safcm/testdata/ssh/sshd/sshd_config.openbsd b/cmd/safcm/testdata/ssh/sshd/sshd_config.openbsd new file mode 100644 index 0000000..76efd8c --- /dev/null +++ b/cmd/safcm/testdata/ssh/sshd/sshd_config.openbsd @@ -0,0 +1,15 @@ +Port 29327 +ListenAddress 127.0.0.1 +PidFile none +LogLevel ERROR + +# Only permit pubkey authentication +PubkeyAuthentication yes +PermitRootLogin prohibit-password +# +ChallengeResponseAuthentication no +HostbasedAuthentication no +KbdInteractiveAuthentication no +PasswordAuthentication no + +# vim: ft=sshdconfig diff --git a/rpc/conn.go b/rpc/conn.go index b2bcf28..6522612 100644 --- a/rpc/conn.go +++ b/rpc/conn.go @@ -33,7 +33,8 @@ type Conn struct { eventsWg sync.WaitGroup debug bool - remote string + sshRemote string + sshOpts []string cmd *exec.Cmd conn *safcm.GobConn diff --git a/rpc/dial.go b/rpc/dial.go index d8a6338..945a75f 100644 --- a/rpc/dial.go +++ b/rpc/dial.go @@ -32,7 +32,7 @@ import ( "ruderich.org/simon/safcm/remote" ) -func (c *Conn) DialSSH(user, host string) error { +func (c *Conn) DialSSH(user, host, sshConfig string) error { if c.events == nil { return fmt.Errorf("cannot reuse Conn") } @@ -48,8 +48,14 @@ func (c *Conn) DialSSH(user, host string) error { // Help debugging by showing executed shell commands opts += "x" } - c.cmd = exec.Command("ssh", remote, "/bin/sh", opts) - c.remote = remote + + c.sshRemote = remote + if sshConfig != "" { + c.sshOpts = append(c.sshOpts, "-F", sshConfig) + } + c.cmd = exec.Command("ssh", + append(append([]string{}, c.sshOpts...), + c.sshRemote, "/bin/sh", opts)...) stdin, err := c.cmd.StdinPipe() if err != nil { @@ -251,10 +257,12 @@ f path = strings.TrimSuffix(path, "\n") c.debugf("DialSSH: uploading new remote to %q at %q", - c.remote, path) + c.sshRemote, path) - cmd := exec.Command("ssh", c.remote, - fmt.Sprintf("cat > %q", path)) + cmd := exec.Command("ssh", + append(append([]string{}, c.sshOpts...), + c.sshRemote, + fmt.Sprintf("cat > %q", path))...) cmd.Stdin = bytes.NewReader(helper) err = c.handleStderrAsEvents(cmd) if err != nil { -- 2.43.2