Files

187 lines
6.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package watchdog
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestAddressFinding(t *testing.T) {
loop := net.ParseIP("127.0.0.1")
if f := AddressFinding("10.211.55.6", []net.IP{loop, net.IPv4(10, 211, 55, 6)}); f != nil {
t.Fatalf("still held: got %+v", f)
}
// The 4-byte form an interface can report is the same address.
if f := AddressFinding("10.211.55.6", []net.IP{{10, 211, 55, 6}}); f != nil {
t.Fatalf("4-byte form: got %+v", f)
}
if f := AddressFinding("", []net.IP{loop}); f != nil {
t.Fatalf("no recorded address: got %+v", f)
}
f := AddressFinding("10.211.55.6", []net.IP{loop, net.ParseIP("10.211.55.9"), net.ParseIP("fe80::1c1a:2bff:fe3c:4d5e")})
if f == nil {
t.Fatal("moved address: no finding")
}
if f.Key != "host-address" || f.Severity != Critical {
t.Fatalf("moved address: key %q severity %v", f.Key, f.Severity)
}
if !strings.Contains(f.SummaryEN, "no longer holds 10.211.55.6") || !strings.Contains(f.SummaryEN, "(it has: 10.211.55.9)") {
t.Fatalf("moved address: summary %q", f.SummaryEN)
}
f = AddressFinding("10.211.55.6", []net.IP{loop})
if f == nil || !strings.Contains(f.Summary, "(现在是:无 / none)") {
t.Fatalf("no address at all: got %+v", f)
}
}
func TestClockFinding(t *testing.T) {
// Status words as <linux/timex.h> spells them: STA_PLL 0x0001, STA_UNSYNC
// 0x0040, STA_NANO 0x2000. An unsynced kernel reports 0x0041 with a daemon
// that has not locked yet, 0x0040 with none; chronyd synced leaves 0x2001.
if f := ClockFinding(0x2001, true); f != nil {
t.Fatalf("synchronized: got %+v", f)
}
for _, st := range []int32{0x0040, 0x0041} {
f := ClockFinding(st, true)
if f == nil || f.Key != "clock" || f.Severity != Warning {
t.Fatalf("status %#x: got %+v", st, f)
}
}
if f := ClockFinding(0x0040, false); f != nil {
t.Fatalf("unreadable status: got %+v", f)
}
}
// certPEM makes a self-signed certificate that expires at notAfter. Only the
// dates, the name and the CA bit matter to CertFinding.
func certPEM(t *testing.T, cn string, notAfter time.Time, ca bool) []byte {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: cn},
NotBefore: notAfter.Add(-365 * 24 * time.Hour),
NotAfter: notAfter,
IsCA: ca,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
}
func writeCert(t *testing.T, path string, pems ...[]byte) {
t.Helper()
if err := os.WriteFile(path, bytes.Join(pems, nil), 0o644); err != nil {
t.Fatal(err)
}
}
func TestCertFinding(t *testing.T) {
now := time.Date(2027, 8, 1, 12, 0, 0, 0, time.UTC)
day := 24 * time.Hour
ca := certPEM(t, "k3s-client-ca@1790445013", now.Add(3650*day), true)
// A fresh install: leaves for a year, the CA for ten. Nothing to say, and
// a missing directory (a host without k3s) is no finding either.
fresh := t.TempDir()
writeCert(t, filepath.Join(fresh, "client-admin.crt"), certPEM(t, "system:admin", now.Add(300*day), false), ca)
if f := CertFinding([]string{fresh, filepath.Join(fresh, "missing")}, now); f != nil {
t.Fatalf("fresh certificates: got %+v", f)
}
if f := CertFinding(nil, now); f != nil {
t.Fatalf("no directories: got %+v", f)
}
// The soonest certificate wins, across directories and within a chained
// file; a key file, garbage and a directory named like a certificate are
// passed over.
server, agent := t.TempDir(), t.TempDir()
writeCert(t, filepath.Join(server, "serving-kube-apiserver.crt"), certPEM(t, "kube-apiserver", now.Add(25*day), false), ca)
writeCert(t, filepath.Join(agent, "client-kubelet.crt"), ca, certPEM(t, "system:node:felis", now.Add(12*day+time.Hour), false))
writeCert(t, filepath.Join(agent, "client-kubelet.key"), certPEM(t, "in a key file", now.Add(time.Hour), false))
writeCert(t, filepath.Join(agent, "garbage.crt"), []byte("-----BEGIN CERTIFICATE-----\nbm90IGEgY2VydA==\n-----END CERTIFICATE-----\n"))
if err := os.Mkdir(filepath.Join(agent, "old.crt"), 0o755); err != nil {
t.Fatal(err)
}
f := CertFinding([]string{server, agent}, now)
if f == nil {
t.Fatal("certificate 12 days from expiry: no finding")
}
if f.Key != "k3s-certs" || f.Severity != Warning || f.For != 0 {
t.Fatalf("12 days: key %q severity %v for %v", f.Key, f.Severity, f.For)
}
want := "the k3s certificate " + filepath.Join(agent, "client-kubelet.crt") + " (system:node:felis) expires at 2027-08-13 13:00 UTC (12 days left)"
if !strings.HasPrefix(f.SummaryEN, want+": ") {
t.Fatalf("12 days: summary %q, want it to start %q", f.SummaryEN, want)
}
if !strings.Contains(f.Summary, "(还剩 12 天)") {
t.Fatalf("12 days: 中文摘要 %q", f.Summary)
}
if !strings.HasPrefix(f.Hint, "sudo systemctl restart k3s") {
t.Fatalf("12 days: hint %q", f.Hint)
}
// The thresholds: a month out is still quiet, the last week is critical,
// and a lapsed certificate says so.
edge := t.TempDir()
for _, tc := range []struct {
left time.Duration
sev Severity
}{
{30 * day, ""},
{30*day - time.Second, Warning},
{7 * day, Warning},
{7*day - time.Second, Critical},
{-time.Hour, Critical},
} {
writeCert(t, filepath.Join(edge, "client-scheduler.crt"), certPEM(t, "system:kube-scheduler", now.Add(tc.left), false))
f := CertFinding([]string{edge}, now)
var got Severity
if f != nil {
got = f.Severity
}
if got != tc.sev {
t.Fatalf("%v left: severity %q, want %q", tc.left, got, tc.sev)
}
}
f = CertFinding([]string{edge}, now)
if !strings.Contains(f.SummaryEN, "(system:kube-scheduler) expired at 2027-08-01 11:00 UTC: ") || strings.Contains(f.SummaryEN, "days left") {
t.Fatalf("expired: summary %q", f.SummaryEN)
}
if !strings.Contains(f.Summary, "已于 2027-08-01 11:00 UTC 过期") {
t.Fatalf("expired: 中文摘要 %q", f.Summary)
}
writeCert(t, filepath.Join(edge, "client-scheduler.crt"), certPEM(t, "system:kube-scheduler", now, false))
if f := CertFinding([]string{edge}, now); f == nil || !strings.Contains(f.SummaryEN, "expires at 2027-08-01 12:00 UTC (0 days left)") {
t.Fatalf("expiring this instant: got %+v", f)
}
// A CA near its end needs a rotation, which a restart does not do.
old := t.TempDir()
writeCert(t, filepath.Join(old, "server-ca.crt"), certPEM(t, "k3s-server-ca@1", now.Add(20*day), true))
f = CertFinding([]string{old}, now)
if f == nil || !strings.Contains(f.Hint, "k3s certificate rotate-ca") || strings.Contains(f.Hint, "systemctl restart") {
t.Fatalf("CA near expiry: got %+v", f)
}
}