Files
edgeguard-native/cmd/edgeguard-ctl/initdb.go
Debian 32ab2c7f47 chore(lint): Backlog auf 0 + golangci-lint als HARTER Gate — v1.3.3
Go-Quality-Baseline-Rollout ABGESCHLOSSEN.

Code-Quality-Backlog (55 → 0):
- errcheck: unbehandelte Close/Rollback/Remove explizit `_ =`; fmt.Sscanf
  `_, _ =` (Zero-Value degradiert sauber).
- unused: toter Code entfernt (nodeIDOrHostname, stripTrailingNewline,
  acme.Service.user, strFold + ungenutzter Import).
- noctx (net/http): http.NewRequestWithContext mit vorhandenem ctx.
- staticcheck: QF1001/S1009/ST1005/SA9003.
- contextcheck: detached-by-design-Stellen mit begründetem //nolint.

Zwei echte Bugs beim Aufräumen gefunden+gefixt:
- backup/remote SFTP-Upload: dst.Close()-Flush-Fehler wurde verschluckt →
  unvollständiges Remote-File galt als Erfolg. Jetzt geprüft+gemeldet.
- haproxy_test: leere if-Assertion (SA9003) testete faktisch nichts →
  echte t.Errorf-Prüfung (kein HSTS für HSTS-disabled Domain).

Bewusste Config-Entscheidungen (.golangci.yml):
- noctx-on-os/exec ausgeschlossen: System-Command-Reloads (systemctl/nft/
  wg/pg) dürfen NICHT an den Request-Context gebunden werden — ein Client-
  Disconnect darf keinen laufenden Reload mitten in der Ausführung killen.
  net/http-noctx bleibt voll aktiv. KEINE exec-Zeile im Code angefasst.
- rowserrcheck/sqlclosecheck raus (database/sql-Linter, bei pgx nur FPs).

Gate scharf gestellt: Makefile release-check ruft golangci-lint jetzt als
HARTEN Gate (install-if-missing, pinned v2.12.2). `make release-check`
grün: vet, golangci-lint, govulncheck, build, test -race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:49:07 +02:00

157 lines
4.3 KiB
Go

package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
)
// cmdInitDB creates the PostgreSQL role and database for edgeguard if
// they don't already exist. Idempotent — safe to re-run from postinst
// on every package upgrade.
//
// Authentication: the local postgres super-user via unix-socket peer
// auth. edgeguard-ctl initdb is invoked from postinst (running as
// root), so it shells out via `sudo -u postgres psql -tA -c ...`.
//
// The created role has LOGIN but no password — production access is
// peer-auth from the system user `edgeguard` over the unix socket
// (architecture.md §6).
//
// Flags:
//
// --db <name> database name (default: edgeguard)
// --role <name> role name (default: edgeguard)
func cmdInitDB(args []string) int {
dbName := "edgeguard"
roleName := "edgeguard"
for i := 0; i < len(args); i++ {
switch args[i] {
case "--db":
if i+1 < len(args) {
dbName = args[i+1]
i++
}
case "--role":
if i+1 < len(args) {
roleName = args[i+1]
i++
}
case "-h", "--help":
fmt.Println("Usage: edgeguard-ctl initdb [--db NAME] [--role NAME]")
return 0
}
}
if !looksLikeIdentifier(dbName) || !looksLikeIdentifier(roleName) {
fmt.Fprintln(os.Stderr, "initdb: --db and --role must match [a-z_][a-z0-9_]{0,62}")
return 2
}
roleExists, err := psqlBoolQuery(fmt.Sprintf(
"SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%s')", roleName))
if err != nil {
fmt.Fprintln(os.Stderr, "initdb: check role:", err)
return 1
}
if roleExists {
fmt.Printf("role %q already exists — leaving it\n", roleName)
} else {
if err := psqlExec(fmt.Sprintf("CREATE ROLE %s LOGIN", roleName)); err != nil {
fmt.Fprintln(os.Stderr, "initdb: create role:", err)
return 1
}
fmt.Printf("created role %q\n", roleName)
}
dbExists, err := psqlBoolQuery(fmt.Sprintf(
"SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = '%s')", dbName))
if err != nil {
fmt.Fprintln(os.Stderr, "initdb: check database:", err)
return 1
}
if dbExists {
fmt.Printf("database %q already exists — leaving it\n", dbName)
} else {
if err := psqlExec(fmt.Sprintf("CREATE DATABASE %s OWNER %s", dbName, roleName)); err != nil {
fmt.Fprintln(os.Stderr, "initdb: create database:", err)
return 1
}
fmt.Printf("created database %q owned by %q\n", dbName, roleName)
}
return 0
}
// looksLikeIdentifier guards against shell- and SQL-injection in the
// role/db flags. Only allow the standard PG identifier shape; we
// interpolate into psql -c so a quoted user-supplied string is
// hostile.
func looksLikeIdentifier(s string) bool {
if s == "" || len(s) > 63 {
return false
}
if s[0] != '_' && (s[0] < 'a' || s[0] > 'z') {
return false
}
for _, r := range s[1:] {
ok := r == '_' ||
(r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9')
if !ok {
return false
}
}
return true
}
// psqlBoolQuery runs a one-shot SELECT EXISTS query as the postgres
// super-user and returns true/false. Tuples-only + unaligned mode
// (-tA) so we get just the literal "t" or "f".
func psqlBoolQuery(sql string) (bool, error) {
out, err := psqlRun([]string{"-tA", "-c", sql})
if err != nil {
return false, err
}
return strings.TrimSpace(string(out)) == "t", nil
}
// psqlExec runs a CREATE ROLE / CREATE DATABASE statement as the
// postgres super-user. ON_ERROR_STOP makes psql return non-zero on
// any SQL error.
func psqlExec(sql string) error {
if _, err := psqlRun([]string{"-v", "ON_ERROR_STOP=1", "-c", sql}); err != nil {
return err
}
return nil
}
// psqlRun shells out to `sudo -u postgres psql` with the given args.
// When edgeguard-ctl initdb is already running as the postgres user
// (e.g. an admin re-running it manually), skip sudo so it works
// without sudoers configuration.
func psqlRun(args []string) ([]byte, error) {
cmd := buildPsqlCmd(args)
var stderr bytes.Buffer
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
msg := strings.TrimSpace(stderr.String())
if msg != "" {
return nil, fmt.Errorf("%w: %s", err, msg)
}
return nil, err
}
return out, nil
}
func buildPsqlCmd(args []string) *exec.Cmd {
if u := os.Getenv("USER"); u == "postgres" {
return exec.Command("psql", args...)
}
full := append([]string{"-n", "-u", "postgres", "psql"}, args...)
return exec.Command("sudo", full...)
}