Merge remote-tracking branch 'origin/main' into secure-ntp

This commit is contained in:
Audiobuzz
2026-07-11 21:03:23 +09:30
9 changed files with 207 additions and 177 deletions
+56 -4
View File
@@ -77,9 +77,15 @@ func main() {
if err != nil {
log.Fatalf("read host key %s: %v", *hostKey, err)
}
princ := splitPrincipals(*principals)
// Principals are the names admins connect by. Default to this host's
// (DHCP-assigned) hostname AND its FQDN <hostname>.<domain> derived from the
// DHCP search domain at runtime (never baked — invariant #1); any explicit
// -principals are added on top. Without the FQDN a `@cert-authority *.<domain>`
// line rejects the cert with "name is not a listed principal" on FQDN logins
// (bench-confirmed on comet-beacon-54, 2026-07-11).
princ := appendUnique(defaultPrincipals(), splitPrincipals(*principals)...)
if len(princ) == 0 {
princ = defaultPrincipals()
princ = []string{"localhost"}
}
keyID := ""
if id.cert.Leaf != nil {
@@ -175,12 +181,58 @@ func splitPrincipals(s string) []string {
return out
}
// defaultPrincipals returns the names admins connect to this host by: the
// (DHCP-assigned) hostname and, when a DHCP search domain is known, the FQDN
// <hostname>.<domain>. The FQDN is derived at runtime (never baked — invariant
// #1), mirroring how the NTS dispatcher derives ntp.<domain>: an admin's
// `@cert-authority *.<domain>` line only matches the host if the FQDN is a cert
// principal, so a short-name-only cert fails FQDN logins.
func defaultPrincipals() []string {
h, err := os.Hostname()
if err != nil || h == "" {
return []string{"localhost"}
return nil
}
return []string{h}
out := []string{h}
if !strings.Contains(h, ".") {
if dom := dhcpDomain(); dom != "" {
out = append(out, h+"."+dom)
}
}
return out
}
// resolvConf is the file dhcpDomain reads (a var so tests can point elsewhere).
var resolvConf = "/etc/resolv.conf"
// dhcpDomain returns the first DHCP-provided search/domain from resolv.conf
// (NetworkManager writes it from DHCP option 15/119). Empty if none is set.
func dhcpDomain() string {
b, err := os.ReadFile(resolvConf)
if err != nil {
return ""
}
for ln := range strings.SplitSeq(string(b), "\n") {
f := strings.Fields(ln)
if len(f) >= 2 && (f[0] == "search" || f[0] == "domain") {
return f[1]
}
}
return ""
}
// appendUnique appends to base each of extra not already present (order-preserving).
func appendUnique(base []string, extra ...string) []string {
seen := make(map[string]bool, len(base))
for _, s := range base {
seen[s] = true
}
for _, s := range extra {
if s != "" && !seen[s] {
seen[s] = true
base = append(base, s)
}
}
return base
}
func envOr(k, def string) string {
+59
View File
@@ -5,6 +5,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
@@ -40,6 +41,64 @@ func TestSplitPrincipals(t *testing.T) {
}
}
func TestDhcpDomain(t *testing.T) {
dir := t.TempDir()
rc := dir + "/resolv.conf"
old := resolvConf
resolvConf = rc
defer func() { resolvConf = old }()
if err := os.WriteFile(rc, []byte("domain corp.example\n"), 0o644); err != nil {
t.Fatal(err)
}
if d := dhcpDomain(); d != "corp.example" {
t.Errorf("domain line: got %q", d)
}
if err := os.WriteFile(rc, []byte("nameserver 1.1.1.1\nsearch a.lab b.lab\n"), 0o644); err != nil {
t.Fatal(err)
}
if d := dhcpDomain(); d != "a.lab" {
t.Errorf("search line: got %q", d)
}
os.Remove(rc)
if d := dhcpDomain(); d != "" {
t.Errorf("missing file: got %q", d)
}
}
func TestDefaultPrincipalsFQDN(t *testing.T) {
h, _ := os.Hostname()
if h == "" || strings.Contains(h, ".") {
t.Skip("host name unsuitable for this test")
}
dir := t.TempDir()
rc := dir + "/resolv.conf"
if err := os.WriteFile(rc, []byte("nameserver 10.0.0.1\nsearch fleet.lab example.org\n"), 0o644); err != nil {
t.Fatal(err)
}
old := resolvConf
resolvConf = rc
defer func() { resolvConf = old }()
got := defaultPrincipals()
if len(got) != 2 || got[0] != h || got[1] != h+".fleet.lab" {
t.Errorf("defaultPrincipals = %v, want [%s %s.fleet.lab]", got, h, h)
}
}
func TestAppendUnique(t *testing.T) {
got := appendUnique([]string{"a", "b"}, "b", "c", "", "a", "d")
want := []string{"a", "b", "c", "d"}
if len(got) != len(want) {
t.Fatalf("got %v want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("got %v want %v", got, want)
}
}
}
func TestParseHandle(t *testing.T) {
h, err := parseHandle("0x81000100")
if err != nil || uint32(h) != 0x81000100 {