release: 0.5.0: Pre-1.0 security review, domain-admin role, doc consolidation
A bounce could be recorded as a successful delivery. The log-tailer's Fixed: - A bounce could be recorded as a successful delivery. The log-tailer's - Log-tailer resumes where it stopped instead of jumping to end-of-file on - Level-2 rate limit no longer overshoots under concurrency: messages that Changed: - The project has a single public home: `github.com/mixeme/selfpost`. Codeberg - Code comments no longer cite the archived specification. References like - `docs/code-review.md` is gone. Its plan is finished — phases 0 (bar the - `docs/architecture.md` gained a *Code layers* section: a diagram of - Phase 1 doc/code hygiene (`docs/code-review.md`): removed ~30 stale … (1 more in CHANGELOG) Security: - Pre-release security review (plan § D, model Fable, 2026-08-06): full pass - `saslpasswd2` argv: the application login is now passed after a `--` Added: - Optional password encryption for the two secret-bearing downloads (plan - Domain import accepts an encrypted export: the envelope is detected by its - `selfpost-backup` writes encrypted archives and reads them back: - `TestParseDelivery` covers the exotic mail.log shapes the review asked for - docs: README *Encrypting a backup or export*; `docs/security.md` § … (14 more in CHANGELOG) Removed: - docs: `security.md` — accepted risk «restore old backup revives session rows» Changed: - docs: `implementation-plan.md` trimmed to the sole open v1.x gate — pre-release - docs: `architecture.md` — sessions (SQLite, idle renew, password change), - docs: `development.md` — expanded e2e stack and `release.yml` matrix workflow. - docs: `security.md` — accepted risk for send-log rows stuck at `queued` after - docs: `roadmap.md` — optional send-log / `mail.log` follow-ups under v1.x tail. … (10 more in CHANGELOG) Full detail: CHANGELOG.md (0.5.0).
This commit is contained in:
+27
-22
@@ -10,11 +10,12 @@ import (
|
||||
)
|
||||
|
||||
// SASLDB manages the Cyrus SASL account database (sasldb2) the panel maintains
|
||||
// for application credentials (spec 5.1). The panel is the only writer; Postfix
|
||||
// reads it to authenticate SMTP clients. Accounts are created and removed with
|
||||
// the standard saslpasswd2 tool ("эквивалент saslpasswd2", per the plan).
|
||||
// for application credentials (architecture.md § Mail path). The panel is the
|
||||
// only writer; Postfix reads it to authenticate SMTP clients. Accounts are
|
||||
// created and removed with the standard saslpasswd2 tool ("эквивалент
|
||||
// saslpasswd2", per the plan).
|
||||
type SASLDB struct {
|
||||
path string // sasldb2 file, under /data so it survives restarts (spec 9)
|
||||
path string // sasldb2 file, under /data so it survives restarts
|
||||
realm string // SASL realm, so lookups match what Postfix's SASL uses
|
||||
|
||||
// run executes saslpasswd2. It is a field so tests can substitute a fake;
|
||||
@@ -28,20 +29,20 @@ type SASLDB struct {
|
||||
|
||||
// NewSASLDB builds a manager for the sasldb2 at path with the given realm. The
|
||||
// realm should match SELFPOST_HOSTNAME so the account identity lines up with
|
||||
// Postfix's SASL configuration in Phase 5.
|
||||
// Postfix's SASL configuration.
|
||||
func NewSASLDB(path, realm string) *SASLDB {
|
||||
return &SASLDB{path: path, realm: realm, run: runSaslpasswd2, dump: dumpSASLDB}
|
||||
}
|
||||
|
||||
// Set creates or updates an application's SASL account with the given password
|
||||
// (spec 5.1, 7.2.9). Used both at creation and when a password is regenerated;
|
||||
// saslpasswd2 overwrites an existing entry in place.
|
||||
// (architecture.md § Mail path). Used both at creation and when a password is
|
||||
// regenerated; saslpasswd2 overwrites an existing entry in place.
|
||||
//
|
||||
// The password is passed to saslpasswd2 on stdin (never as an argument, so it
|
||||
// cannot leak through the process table or logs). The login is passed as a
|
||||
// separate argv element after being whitelisted by validateLogin — it never
|
||||
// goes through a shell and is never interpolated into a command string (spec
|
||||
// 7.6.3).
|
||||
// goes through a shell and is never interpolated into a command string
|
||||
// (security.md).
|
||||
func (s *SASLDB) Set(login, password string) error {
|
||||
if err := validateLogin(login); err != nil {
|
||||
return err
|
||||
@@ -50,21 +51,24 @@ func (s *SASLDB) Set(login, password string) error {
|
||||
// -c: create the account / set the password.
|
||||
// -f: operate on our sasldb2 rather than the system default path.
|
||||
// -u: the realm the account lives under.
|
||||
args := []string{"-p", "-c", "-f", s.path, "-u", s.realm, login}
|
||||
// --: end of options, so a login can never be parsed as a flag (the
|
||||
// whitelist already forbids nothing that getopt would eat, but a login
|
||||
// starting with '-' is legal there — this keeps it an operand).
|
||||
args := []string{"-p", "-c", "-f", s.path, "-u", s.realm, "--", login}
|
||||
if err := s.run(args, []byte(password)); err != nil {
|
||||
return fmt.Errorf("saslpasswd2 set %q: %w", login, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes an application's SASL account (spec 7.2.8). A missing account
|
||||
// Delete removes an application's SASL account (product.md). A missing account
|
||||
// is not treated as an error, so deletion is idempotent and safe to retry.
|
||||
func (s *SASLDB) Delete(login string) error {
|
||||
if err := validateLogin(login); err != nil {
|
||||
return err
|
||||
}
|
||||
// -d: delete the account.
|
||||
args := []string{"-d", "-f", s.path, "-u", s.realm, login}
|
||||
// -d: delete the account. "--" as in Set: the login is always an operand.
|
||||
args := []string{"-d", "-f", s.path, "-u", s.realm, "--", login}
|
||||
if err := s.run(args, nil); err != nil {
|
||||
return fmt.Errorf("saslpasswd2 delete %q: %w", login, err)
|
||||
}
|
||||
@@ -76,15 +80,16 @@ func (s *SASLDB) Delete(login string) error {
|
||||
var ErrSecretNotFound = fmt.Errorf("sasl secret not found")
|
||||
|
||||
// Secret returns an application's stored password so it can be carried in a
|
||||
// domain export and re-created verbatim on another instance (spec 7.5.B). This
|
||||
// is possible because sasldb2 keeps the SASL secret in a password-equivalent
|
||||
// form (the plaintext userPassword property, to serve challenge-response
|
||||
// mechanisms) — unlike the admin's one-way bcrypt hash (spec 7.6). The value is
|
||||
// realm-independent, so the importer can re-key it under its own realm.
|
||||
// domain export and re-created verbatim on another instance (architecture.md §
|
||||
// Persistence). This is possible because sasldb2 keeps the SASL secret in a
|
||||
// password-equivalent form (the plaintext userPassword property, to serve
|
||||
// challenge-response mechanisms) — unlike the admin's one-way bcrypt hash
|
||||
// (security.md). The value is realm-independent, so the importer can re-key it
|
||||
// under its own realm.
|
||||
//
|
||||
// It reads the database with db_dump (Berkeley DB), passing only our own file
|
||||
// path as a fixed argument (no shell, no user input — spec 7.6.3), and returns
|
||||
// ErrSecretNotFound if the login has no entry.
|
||||
// path as a fixed argument (no shell, no user input — security.md), and
|
||||
// returns ErrSecretNotFound if the login has no entry.
|
||||
func (s *SASLDB) Secret(login string) (string, error) {
|
||||
if err := validateLogin(login); err != nil {
|
||||
return "", err
|
||||
@@ -155,7 +160,7 @@ func parseSASLSecret(dump []byte, login, realm string) (string, bool, error) {
|
||||
|
||||
// runSaslpasswd2 executes the real saslpasswd2 with the given arguments and
|
||||
// stdin. Arguments are passed as a fixed argv (no shell), so no user input is
|
||||
// ever interpreted as a command (spec 7.6.3).
|
||||
// ever interpreted as a command (security.md).
|
||||
func runSaslpasswd2(args []string, stdin []byte) error {
|
||||
cmd := exec.Command("saslpasswd2", args...)
|
||||
if stdin != nil {
|
||||
@@ -170,7 +175,7 @@ func runSaslpasswd2(args []string, stdin []byte) error {
|
||||
|
||||
// dumpSASLDB runs db_dump to export the sasldb2 as key/value hex pairs. The path
|
||||
// is our own sasldb2 file (never user input) and is passed as a fixed argument
|
||||
// with no shell (spec 7.6.3).
|
||||
// with no shell (security.md).
|
||||
func dumpSASLDB(path string) ([]byte, error) {
|
||||
cmd := exec.Command("db_dump", path)
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
Reference in New Issue
Block a user