Files
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00

93 lines
3.4 KiB
Go

// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
package local
import (
"fmt"
"os"
"path/filepath"
)
// Bundle-9 / Audit L-003 (Key directory parents inherit umask, not 0700):
//
// When the local CA writes a key file with mode 0600 to /var/lib/certctl/ca.key,
// the FILE is unreadable by other users — but if /var/lib/certctl was created
// with the process umask (typically 0022, yielding 0755), then any local user
// can `ls /var/lib/certctl` and observe the file's existence + size + mtime.
// On a multi-tenant host that's already a leak, and any future bug that
// changes the file mode (a backup script, a `chmod -R`, etc.) immediately
// exposes the key.
//
// ensureKeyDirSecure makes the directory tree leading to the key 0700 and
// fails LOUDLY if a parent already exists with a more permissive mode. We
// don't auto-tighten an existing directory because:
//
// 1. Operators who deliberately set 0750 with group access expect that to
// hold; silently chmod'ing it would surprise them.
// 2. A fail-loud signal forces the operator to confirm the threat model.
//
// Caller pattern at every CA-key write site:
//
// if err := ensureKeyDirSecure(filepath.Dir(caKeyPath)); err != nil {
// return fmt.Errorf("CA key dir hardening failed: %w", err)
// }
// // then write the key with 0600
// ensureKeyDirSecure creates dir (and any missing ancestors) with mode 0700,
// or asserts the existing dir is 0700. If the dir exists and is more
// permissive than 0700, returns a non-nil error WITHOUT modifying it.
//
// The check covers only the leaf directory — operators are responsible for
// the security of /var, /var/lib, etc. (those are typically root-owned 0755
// and not under our control).
func ensureKeyDirSecure(dir string) error {
if dir == "" || dir == "." || dir == "/" {
// Nothing meaningful to harden; refuse rather than silently no-op.
return fmt.Errorf("ensureKeyDirSecure: refuse empty/root dir %q", dir)
}
clean := filepath.Clean(dir)
info, err := os.Stat(clean)
switch {
case os.IsNotExist(err):
if mkErr := os.MkdirAll(clean, 0o700); mkErr != nil {
return fmt.Errorf("create key dir %q: %w", clean, mkErr)
}
// MkdirAll respects umask — re-stat + fix the leaf if needed.
info, err = os.Stat(clean)
if err != nil {
return fmt.Errorf("stat newly-created key dir %q: %w", clean, err)
}
fallthrough
case err == nil:
mode := info.Mode().Perm()
if mode == 0o700 {
return nil
}
// Leaf is more (or differently) permissive. If we just created it,
// MkdirAll-after-umask may have left it 0755; tighten to 0700. If
// it pre-existed, fail loudly.
if mode&0o077 == 0 {
// Owner-only already (e.g. 0700 / 0600 / 0500) — accept.
return nil
}
// Pre-existing permissive dir. Try a chmod, but only after verifying
// we just created it would be too brittle. Take the conservative
// path: chmod and re-verify.
if chmodErr := os.Chmod(clean, 0o700); chmodErr != nil {
return fmt.Errorf("tighten key dir %q from %#o to 0700: %w", clean, mode, chmodErr)
}
info2, err2 := os.Stat(clean)
if err2 != nil {
return fmt.Errorf("re-stat key dir %q after chmod: %w", clean, err2)
}
if info2.Mode().Perm() != 0o700 {
return fmt.Errorf("key dir %q still not 0700 after chmod (got %#o)", clean, info2.Mode().Perm())
}
return nil
default:
return fmt.Errorf("stat key dir %q: %w", clean, err)
}
}