From 41f6a636875f3ca327fd699aae358ef3eedad0be Mon Sep 17 00:00:00 2001 From: peio Date: Wed, 21 Jan 2026 22:54:20 +0000 Subject: [PATCH] feature: switch to simple user --- cmd/mailcloak/main.go | 21 +++++++++-- configs/config.yaml.sample | 3 ++ internal/mailcloak/config.go | 7 ++++ internal/mailcloak/policy.go | 22 +++++++++--- internal/mailcloak/privileges.go | 60 ++++++++++++++++++++++++++++++++ internal/mailcloak/socketmap.go | 22 +++++++++--- 6 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 internal/mailcloak/privileges.go diff --git a/cmd/mailcloak/main.go b/cmd/mailcloak/main.go index 1a04b74..62886b7 100644 --- a/cmd/mailcloak/main.go +++ b/cmd/mailcloak/main.go @@ -30,6 +30,23 @@ func main() { log.Fatalf("config: %v", err) } + policyListener, err := mailcloak.OpenPolicyListener(cfg) + if err != nil { + log.Fatalf("policy listener: %v", err) + } + + socketmapListener, err := mailcloak.OpenSocketmapListener(cfg) + if err != nil { + _ = policyListener.Close() + log.Fatalf("socketmap listener: %v", err) + } + + if err := mailcloak.DropPrivileges(cfg); err != nil { + _ = policyListener.Close() + _ = socketmapListener.Close() + log.Fatalf("privileges: %v", err) + } + db, err := mailcloak.OpenAliasDB(cfg.SQLite.Path) if err != nil { log.Fatalf("sqlite: %v", err) @@ -44,14 +61,14 @@ func main() { // Start socketmap server go func() { - if err := mailcloak.RunSocketmap(ctx, cfg, db); err != nil { + if err := mailcloak.ServeSocketmap(ctx, cfg, db, socketmapListener); err != nil { log.Fatalf("socketmap: %v", err) } }() // Start policy server go func() { - if err := mailcloak.RunPolicy(ctx, cfg, db, kc, cache); err != nil { + if err := mailcloak.ServePolicy(ctx, cfg, db, kc, cache, policyListener); err != nil { log.Fatalf("policy: %v", err) } }() diff --git a/configs/config.yaml.sample b/configs/config.yaml.sample index c2f3286..ec22de5 100644 --- a/configs/config.yaml.sample +++ b/configs/config.yaml.sample @@ -24,3 +24,6 @@ sockets: socket_owner_user: "postfix" socket_owner_group: "postfix" socket_mode: "0660" + +daemon: + user: "mailcloak" diff --git a/internal/mailcloak/config.go b/internal/mailcloak/config.go index e27dfa7..75b6213 100644 --- a/internal/mailcloak/config.go +++ b/internal/mailcloak/config.go @@ -8,6 +8,10 @@ import ( ) type Config struct { + Daemon struct { + User string `yaml:"user"` + } `yaml:"daemon"` + Keycloak struct { BaseURL string `yaml:"base_url"` Realm string `yaml:"realm"` @@ -58,5 +62,8 @@ func LoadConfig(path string) (*Config, error) { if cfg.Policy.KeycloakFailureMode == "" { cfg.Policy.KeycloakFailureMode = "tempfail" } + if cfg.Daemon.User == "" { + cfg.Daemon.User = "mailcloak" + } return &cfg, nil } diff --git a/internal/mailcloak/policy.go b/internal/mailcloak/policy.go index 11e0195..4dcf30d 100644 --- a/internal/mailcloak/policy.go +++ b/internal/mailcloak/policy.go @@ -38,20 +38,26 @@ func (c *Cache) Put(key, val string, ok bool) { c.m[key] = cacheItem{val: val, ok: ok, expires: time.Now().Add(c.ttl)} } -func RunPolicy(ctx context.Context, cfg *Config, db *AliasDB, kc *Keycloak, cache *Cache) error { +func OpenPolicyListener(cfg *Config) (net.Listener, error) { sock := cfg.Sockets.PolicySocket _ = os.Remove(sock) l, err := net.Listen("unix", sock) if err != nil { - return err + return nil, err } - defer l.Close() if err := ChownChmodSocket(sock, cfg); err != nil { - return err + _ = l.Close() + return nil, err } + return l, nil +} + +func ServePolicy(ctx context.Context, cfg *Config, db *AliasDB, kc *Keycloak, cache *Cache, l net.Listener) error { + defer l.Close() + for { conn, err := l.Accept() if err != nil { @@ -66,6 +72,14 @@ func RunPolicy(ctx context.Context, cfg *Config, db *AliasDB, kc *Keycloak, cach } } +func RunPolicy(ctx context.Context, cfg *Config, db *AliasDB, kc *Keycloak, cache *Cache) error { + l, err := OpenPolicyListener(cfg) + if err != nil { + return err + } + return ServePolicy(ctx, cfg, db, kc, cache, l) +} + func handlePolicyConn(conn net.Conn, cfg *Config, db *AliasDB, kc *Keycloak, cache *Cache) { defer conn.Close() r := bufio.NewReader(conn) diff --git a/internal/mailcloak/privileges.go b/internal/mailcloak/privileges.go new file mode 100644 index 0000000..20b66fb --- /dev/null +++ b/internal/mailcloak/privileges.go @@ -0,0 +1,60 @@ +package mailcloak + +import ( + "fmt" + "os" + "os/user" + "strconv" + "syscall" +) + +func DropPrivileges(cfg *Config) error { + userName := cfg.Daemon.User + if userName == "" { + userName = "mailcloak" + } + + u, err := user.Lookup(userName) + if err != nil { + return fmt.Errorf("lookup user %s: %w", userName, err) + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + return fmt.Errorf("bad uid for %s: %w", userName, err) + } + gid, err := strconv.Atoi(u.Gid) + if err != nil { + return fmt.Errorf("bad gid for %s: %w", userName, err) + } + + euid := os.Geteuid() + if euid == uid { + return nil + } + if euid != 0 { + return fmt.Errorf("must run as root to switch user to %s", userName) + } + + if groups, err := u.GroupIds(); err == nil { + gids := make([]int, 0, len(groups)) + for _, g := range groups { + if id, err := strconv.Atoi(g); err == nil { + gids = append(gids, id) + } + } + if len(gids) > 0 { + if err := syscall.Setgroups(gids); err != nil { + return fmt.Errorf("setgroups: %w", err) + } + } + } + + if err := syscall.Setgid(gid); err != nil { + return fmt.Errorf("setgid: %w", err) + } + if err := syscall.Setuid(uid); err != nil { + return fmt.Errorf("setuid: %w", err) + } + + return nil +} diff --git a/internal/mailcloak/socketmap.go b/internal/mailcloak/socketmap.go index 01e0e15..3717064 100644 --- a/internal/mailcloak/socketmap.go +++ b/internal/mailcloak/socketmap.go @@ -11,20 +11,26 @@ import ( "strings" ) -func RunSocketmap(ctx context.Context, cfg *Config, db *AliasDB) error { +func OpenSocketmapListener(cfg *Config) (net.Listener, error) { sock := cfg.Sockets.SocketmapSocket _ = os.Remove(sock) l, err := net.Listen("unix", sock) if err != nil { - return err + return nil, err } - defer l.Close() if err := ChownChmodSocket(sock, cfg); err != nil { - return err + _ = l.Close() + return nil, err } + return l, nil +} + +func ServeSocketmap(ctx context.Context, cfg *Config, db *AliasDB, l net.Listener) error { + defer l.Close() + for { conn, err := l.Accept() if err != nil { @@ -39,6 +45,14 @@ func RunSocketmap(ctx context.Context, cfg *Config, db *AliasDB) error { } } +func RunSocketmap(ctx context.Context, cfg *Config, db *AliasDB) error { + l, err := OpenSocketmapListener(cfg) + if err != nil { + return err + } + return ServeSocketmap(ctx, cfg, db, l) +} + // Postfix socketmap framing: ":," func handleSocketmapConn(conn net.Conn, cfg *Config, db *AliasDB) { defer conn.Close()