Skip to content

Commit 93668d8

Browse files
jimmykarilyclaude
andcommitted
ci: add ssh-hardening job running DevSec ssh-baseline
Sketches a TDD-first pipeline for #789. The upstream DevSec ssh-baseline InSpec profile is executed against a booted Kairos VM via cinc-auditor (FOSS drop-in for chef/inspec). Nothing gets installed inside the image; the profile runs from the CI runner over SSH port-forwarded by QEMU. The job is expected to fail on master until sshd hardening lands in kairos-init: that failure is the specification. - tests/ssh_hardening_test.go: Ginkgo test labelled ssh-hardening that shells out to cinc-auditor against 127.0.0.1:$SSH_PORT. - tests/tests_suite_test.go: honour SSH_PORT env for pinning the host port when set (fall back to random allocation otherwise). - .github/workflows/reusable-qemu-test.yaml: install cinc-auditor and pin SSH_PORT=22222 when inputs.test == ssh-hardening. - .github/workflows/image-pr.yaml: add ssh-hardening to the core-tests matrix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
1 parent 73b8a47 commit 93668d8

4 files changed

Lines changed: 86 additions & 2 deletions

File tree

.github/workflows/image-pr.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
- test: "reset"
7777
- test: "upgrade-with-cli"
7878
- test: "bundles"
79+
- test: "ssh-hardening"
7980
- test: "upgrade-latest-with-cli"
8081
release-matcher: "kairos-hadron-*-core-amd64-generic-v*.iso"
8182
standard-tests:

.github/workflows/reusable-qemu-test.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ jobs:
140140
sudo mkdir -p /etc/luet/repos.conf.d/
141141
sudo luet repo add -y kairos --url quay.io/kairos/packages --type docker
142142
LUET_NOLOCK=true sudo -E luet install -y container/kubectl utils/k3d
143+
- name: Install cinc-auditor for the DevSec ssh-baseline profile
144+
# cinc-auditor is a FOSS drop-in for chef/inspec (InSpec >= 5 is no
145+
# longer under an OSS licence). It runs the profile from
146+
# https://github.com/dev-sec/ssh-baseline against the booted VM over
147+
# SSH; nothing gets installed inside the image.
148+
if: ${{ inputs.test == 'ssh-hardening' }}
149+
run: |
150+
curl -L https://omnitruck.cinc.sh/install.sh | sudo bash -s -- -P cinc-auditor
151+
cinc-auditor --version
143152
- name: Setup bridge network for VM-to-VM communication
144153
if: inputs.use_bridge_network
145154
run: |
@@ -203,6 +212,9 @@ jobs:
203212
CPUS: 4
204213
CONTAINER_IMAGE: ${{ env.IMAGE_NAME }}
205214
USE_BRIDGE_NETWORK: ${{ inputs.use_bridge_network && '1' || '' }}
215+
# ssh-hardening pins the SSH port so cinc-auditor (running on the
216+
# runner) and the QEMU port-forward agree on where to connect.
217+
SSH_PORT: ${{ inputs.test == 'ssh-hardening' && '22222' || '' }}
206218
run: |
207219
if [ "${{ inputs.secureboot }}" = "true" ]; then
208220
export FIRMWARE=${{ steps.find_ovmf.outputs.firmware }}

tests/ssh_hardening_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package mos_test
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
. "github.com/spectrocloud/peg/matcher"
12+
)
13+
14+
// The DevSec SSH baseline is the reference spec we harden against.
15+
// See: https://github.com/dev-sec/ssh-baseline
16+
//
17+
// This test does not translate the baseline into Ginkgo assertions.
18+
// It runs the upstream InSpec profile against a booted Kairos VM via
19+
// cinc-auditor (a FOSS drop-in for chef/inspec, since InSpec >= 5 is
20+
// no longer under an OSS licence). The runner must have cinc-auditor
21+
// installed; the CI job in .github/workflows/reusable-qemu-test.yaml
22+
// takes care of that when the test label is "ssh-hardening".
23+
//
24+
// This test is expected to FAIL on master until the ssh_hardening
25+
// profile lands in kairos-init. That is the point: the pipeline is
26+
// the spec, in TDD order.
27+
var _ = Describe("ssh hardening", Label("ssh-hardening"), func() {
28+
var vm VM
29+
30+
BeforeEach(func() {
31+
_, vm = startVM()
32+
vm.EventuallyConnects(1200)
33+
})
34+
35+
AfterEach(func() {
36+
if CurrentSpecReport().Failed() {
37+
serial, _ := os.ReadFile(filepath.Join(vm.StateDir, "serial.log"))
38+
_ = os.MkdirAll("logs", 0o755)
39+
_ = os.WriteFile(filepath.Join("logs", "serial.log"), serial, 0o644)
40+
}
41+
Expect(vm.Destroy(nil)).ToNot(HaveOccurred())
42+
})
43+
44+
It("passes the DevSec ssh-baseline profile", func() {
45+
sshPort := os.Getenv("SSH_PORT")
46+
Expect(sshPort).ToNot(BeEmpty(), "SSH_PORT must be set by the CI job so cinc-auditor can reach the VM")
47+
48+
reportDir := "logs"
49+
Expect(os.MkdirAll(reportDir, 0o755)).To(Succeed())
50+
reportPath := filepath.Join(reportDir, "ssh-baseline.json")
51+
52+
cmd := exec.Command("cinc-auditor", "exec",
53+
"https://github.com/dev-sec/ssh-baseline",
54+
"--target", fmt.Sprintf("ssh://%s@127.0.0.1:%s", user(), sshPort),
55+
"--password", pass(),
56+
"--reporter", "cli", "json:"+reportPath,
57+
"--chef-license", "accept-silent",
58+
)
59+
cmd.Env = append(os.Environ(), "TERM=dumb")
60+
out, err := cmd.CombinedOutput()
61+
GinkgoWriter.Printf("cinc-auditor output:\n%s\n", string(out))
62+
63+
Expect(err).ToNot(HaveOccurred(),
64+
"ssh-baseline profile reported failures; see %s for the JSON report", reportPath)
65+
})
66+
})

tests/tests_suite_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,13 @@ func defaultVMOptsNoDrives(stateDir string) []types.MachineOption {
417417
// Always setup a tpm emulator
418418
emulateTPM(stateDir)
419419

420-
sshPort, err = getFreePort()
421-
Expect(err).ToNot(HaveOccurred())
420+
if p := os.Getenv("SSH_PORT"); p != "" {
421+
sshPort, err = strconv.Atoi(p)
422+
Expect(err).ToNot(HaveOccurred())
423+
} else {
424+
sshPort, err = getFreePort()
425+
Expect(err).ToNot(HaveOccurred())
426+
}
422427
GinkgoLogr.Info("Got SSH port", "port", sshPort, "vm", vmName)
423428

424429
memory := getEnvOrDefault("MEMORY", "2048")

0 commit comments

Comments
 (0)