aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--cmd/mirumd/config.go2+1 −1
-rw-r--r--cmd/mirumd/main.go8+5 −3
-rw-r--r--cmd/mirumd/server.go18+18 −0
-rw-r--r--cmd/mirumd/server_grpc.go35+18 −17
-rw-r--r--cmd/mirumd/server_web.go121+119 −2
-rw-r--r--cmd/mirumd/templates/index.html16+16 −0
-rw-r--r--cmd/mirumd/templates/layout.html14+14 −0
-rw-r--r--cmd/mirumd/templates/login.html21+21 −0
-rw-r--r--internal/database/database.go197+177 −20
-rw-r--r--pkg/mirumd.service4+2 −2
-rw-r--r--pkg/mirumd.yaml6+3 −3
11 files changed, 394 insertions, 48 deletions
diff --git a/cmd/mirumd/config.go b/cmd/mirumd/config.go
index c856d01..7eae4e3 100644
--- a/cmd/mirumd/config.go
+++ b/cmd/mirumd/config.go
@@ -15,7 +15,7 @@ type config struct {
GrpcAddr string `yaml:"grpc_addr"`
AdminSocket string `yaml:"admin_socket"`
DatabaseUri string `yaml:"database_uri"`
- WorkerSecret string `yaml:"secret"`
+ WorkerSecret string `yaml:"worker_secret"`
Pepper string `yaml:"pepper"`
GitHubToken string `yaml:"token"`
diff --git a/cmd/mirumd/main.go b/cmd/mirumd/main.go
index 1a048d7..9c413f8 100644
--- a/cmd/mirumd/main.go
+++ b/cmd/mirumd/main.go
@@ -159,6 +159,8 @@ func daemon(configFile, socketFlag string) {
}
}()
+ go srv.PurgeSessions(ctx)
+
sup.Ready()
go sup.StartWatchdog(ctx)
@@ -175,12 +177,12 @@ func daemon(configFile, socketFlag string) {
srv.Close()
adminSrv.GracefulStop()
- wwwSrv.Shutdown(ctx)
+ wwwSrv.Shutdown(context.Background())
grpcSrv.GracefulStop()
}
-// listeners returns gRPC and HTTP listeners.
-// With systemd socket activation it expects two named fds: "grpc" and "http".
+// listeners returns gRPC, web, and admin listeners.
+// With systemd socket activation it expects two named fds: "grpc" and "web".
// Without socket activation it falls back to configured addresses.
func listeners(cfg *config) (grpcLn, webLn, adminLn net.Listener, err error) {
named, err := activation.ListenersWithNames()
diff --git a/cmd/mirumd/server.go b/cmd/mirumd/server.go
index 2d7b524..a93c9b8 100644
--- a/cmd/mirumd/server.go
+++ b/cmd/mirumd/server.go
@@ -9,6 +9,7 @@ import (
"log/slog"
"sync"
"sync/atomic"
+ "time"
"dimidiumlabs/mirum/internal/database"
"dimidiumlabs/mirum/internal/forges"
@@ -30,6 +31,23 @@ func (s *server) Close() {
close(s.queue)
}
+// PurgeSessions periodically deletes expired sessions until ctx is cancelled.
+func (s *server) PurgeSessions(ctx context.Context) {
+ ticker := time.NewTicker(1 * time.Hour)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ if err := s.db.PurgeExpiredSessions(ctx); err != nil {
+ slog.Error("purge sessions", "err", err)
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
func (s *server) enqueue(ev *forges.PushEvent) string {
s.taskCounter.Add(1)
id := fmt.Sprintf("task-%d", s.taskCounter.Load())
diff --git a/cmd/mirumd/server_grpc.go b/cmd/mirumd/server_grpc.go
index 82063cf..6490715 100644
--- a/cmd/mirumd/server_grpc.go
+++ b/cmd/mirumd/server_grpc.go
@@ -8,6 +8,7 @@ import (
"fmt"
"log/slog"
"sync"
+ "sync/atomic"
"time"
"dimidiumlabs/mirum/internal/protocol"
@@ -15,12 +16,14 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
- "google.golang.org/grpc/peer"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
+// connIDKey is the context key for the unique connection identifier.
+type connIDKey struct{}
+
func NewGrpcServer(ctx context.Context, srv *server, secret []byte) *grpc.Server {
gsrv := &grpcService{
ctx: ctx,
@@ -43,7 +46,8 @@ type grpcService struct {
ctx context.Context
srv *server
secret []byte
- authedPeers sync.Map // peer addr string → true
+ authedConns sync.Map // conn ID (uint64) → true
+ nextConnID atomic.Uint64
}
func (g *grpcService) Poll(ctx context.Context, req *pb.PollRequest) (*pb.Task, error) {
@@ -128,8 +132,8 @@ func (g *grpcService) Handshake(stream pb.Mirum_HandshakeServer) error {
)
// Step 4: accept
- if p, ok := peer.FromContext(stream.Context()); ok {
- g.authedPeers.Store(p.Addr.String(), true)
+ if id, ok := stream.Context().Value(connIDKey{}).(uint64); ok {
+ g.authedConns.Store(id, true)
}
return g.sendResult(stream, nil, warnings)
}
@@ -154,18 +158,15 @@ func (g *grpcService) reject(stream pb.Mirum_HandshakeServer, reason string) err
return fmt.Errorf("%s", reason)
}
-func peerAddr(ctx context.Context) string {
- if p, ok := peer.FromContext(ctx); ok {
- return p.Addr.String()
- }
- return ""
-}
-
func (g *grpcService) requireAuth(ctx context.Context, method string) error {
if method == pb.Mirum_Handshake_FullMethodName {
return nil
}
- if _, ok := g.authedPeers.Load(peerAddr(ctx)); !ok {
+ id, ok := ctx.Value(connIDKey{}).(uint64)
+ if !ok {
+ return status.Error(codes.Unauthenticated, "handshake required")
+ }
+ if _, ok := g.authedConns.Load(id); !ok {
return status.Error(codes.Unauthenticated, "handshake required")
}
return nil
@@ -202,7 +203,8 @@ type connTracker struct {
}
func (t *connTracker) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
- return ctx
+ id := t.gsrv.nextConnID.Add(1)
+ return context.WithValue(ctx, connIDKey{}, id)
}
func (t *connTracker) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
@@ -213,10 +215,9 @@ func (t *connTracker) HandleRPC(ctx context.Context, s stats.RPCStats) {}
func (t *connTracker) HandleConn(ctx context.Context, s stats.ConnStats) {
if _, ok := s.(*stats.ConnEnd); ok {
- addr := peerAddr(ctx)
- if addr != "" {
- t.gsrv.authedPeers.Delete(addr)
- slog.Debug("peer disconnected", "addr", addr)
+ if id, ok := ctx.Value(connIDKey{}).(uint64); ok {
+ t.gsrv.authedConns.Delete(id)
+ slog.Debug("peer disconnected", "conn", id)
}
}
}
diff --git a/cmd/mirumd/server_web.go b/cmd/mirumd/server_web.go
index d8ed590..5c922c6 100644
--- a/cmd/mirumd/server_web.go
+++ b/cmd/mirumd/server_web.go
@@ -5,15 +5,68 @@ package main
import (
"context"
+ "crypto/rand"
+ "crypto/subtle"
+ "embed"
+ "encoding/base64"
"errors"
- "fmt"
+ "html/template"
"io"
"net"
"net/http"
+ "dimidiumlabs/mirum/internal/database"
"dimidiumlabs/mirum/internal/forges"
)
+//go:embed templates/*.html
+var templateFS embed.FS
+
+var (
+ indexTmpl = template.Must(template.ParseFS(templateFS, "templates/layout.html", "templates/index.html"))
+ loginTmpl = template.Must(template.ParseFS(templateFS, "templates/layout.html", "templates/login.html"))
+)
+
+// csrfToken returns the current CSRF token, setting a cookie if absent.
+func csrfToken(w http.ResponseWriter, r *http.Request) string {
+ if c, err := r.Cookie("csrf"); err == nil && c.Value != "" {
+ return c.Value
+ }
+ b := make([]byte, 32)
+ rand.Read(b)
+ token := base64.RawURLEncoding.EncodeToString(b)
+ http.SetCookie(w, &http.Cookie{
+ Name: "csrf",
+ Value: token,
+ Path: "/",
+ HttpOnly: true,
+ Secure: true,
+ SameSite: http.SameSiteStrictMode,
+ })
+ return token
+}
+
+// csrfOK checks that the form field matches the cookie (double-submit).
+func csrfOK(r *http.Request) bool {
+ cookie, err := r.Cookie("csrf")
+ if err != nil || cookie.Value == "" {
+ return false
+ }
+ field := r.FormValue("csrf")
+ return subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(field)) == 1
+}
+
+func clearCookie(w http.ResponseWriter, name string) {
+ http.SetCookie(w, &http.Cookie{
+ Name: name,
+ Value: "",
+ Path: "/",
+ HttpOnly: true,
+ Secure: true,
+ MaxAge: -1,
+ })
+}
+
func NewWwwServer(ctx context.Context, srv *server) *http.Server {
return &http.Server{
Handler: wwwRoutes(srv),
@@ -28,7 +81,14 @@ func wwwRoutes(srv *server) http.Handler {
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprint(w, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Mirum</title></head><body><h1>Mirum</h1><p>CI server is running.</p></body></html>`)
+ var data struct{ Email, CSRF string }
+ if c, err := r.Cookie("session"); err == nil {
+ if sess, err := srv.db.GetSession(r.Context(), c.Value); err == nil {
+ data.Email = sess.Email
+ data.CSRF = csrfToken(w, r)
+ }
+ }
+ indexTmpl.ExecuteTemplate(w, "layout", data)
})
mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
@@ -56,5 +116,62 @@ func wwwRoutes(srv *server) http.Handler {
w.WriteHeader(http.StatusAccepted)
})
+ mux.HandleFunc("GET /auth/login", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ loginTmpl.ExecuteTemplate(w, "layout", map[string]string{"CSRF": csrfToken(w, r)})
+ })
+
+ mux.HandleFunc("POST /auth/login", func(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, 4096)
+ if !csrfOK(r) {
+ clearCookie(w, "csrf")
+ http.Error(w, "invalid csrf token", http.StatusForbidden)
+ return
+ }
+
+ email := r.FormValue("email")
+ password := r.FormValue("password")
+
+ userID, err := srv.db.VerifyPassword(r.Context(), email, password, []byte(srv.cfg.Pepper))
+ if err != nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ loginTmpl.ExecuteTemplate(w, "layout", map[string]string{
+ "Error": "Invalid credentials",
+ "CSRF": csrfToken(w, r),
+ })
+ return
+ }
+
+ token, err := srv.db.CreateSession(r.Context(), userID)
+ if err != nil {
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+
+ http.SetCookie(w, &http.Cookie{
+ Name: "session",
+ Value: token,
+ Path: "/",
+ HttpOnly: true,
+ Secure: true,
+ SameSite: http.SameSiteLaxMode,
+ MaxAge: int(database.SessionTTL.Seconds()),
+ })
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ })
+
+ mux.HandleFunc("POST /auth/logout", func(w http.ResponseWriter, r *http.Request) {
+ if !csrfOK(r) {
+ http.Error(w, "invalid csrf token", http.StatusForbidden)
+ return
+ }
+ if c, err := r.Cookie("session"); err == nil {
+ srv.db.DeleteSession(r.Context(), c.Value)
+ }
+ clearCookie(w, "session")
+ clearCookie(w, "csrf")
+ http.Redirect(w, r, "/auth/login", http.StatusSeeOther)
+ })
+
return mux
}
diff --git a/cmd/mirumd/templates/index.html b/cmd/mirumd/templates/index.html
new file mode 100644
--- /dev/null
+++ b/cmd/mirumd/templates/index.html
@@ -0,0 +1,16 @@
+{{/* Copyright (c) 2026 Nikolay Govorov */}}
+{{/* SPDX-License-Identifier: AGPL-3.0-or-later */}}
+{{define "title"}}Home{{end}}
+{{define "content"}}
+<h1>Mirum</h1>
+{{if .Email}}
+<p>Logged in as {{.Email}}</p>
+<form method="POST" action="/auth/logout">
+<input type="hidden" name="csrf" value="{{.CSRF}}">
+<button type="submit">Logout</button>
+</form>
+{{else}}
+<p>CI server is running.</p>
+<p><a href="/auth/login">Login</a></p>
+{{end}}
+{{end}}
diff --git a/cmd/mirumd/templates/layout.html b/cmd/mirumd/templates/layout.html
new file mode 100644
--- /dev/null
+++ b/cmd/mirumd/templates/layout.html
@@ -0,0 +1,14 @@
+{{/* Copyright (c) 2026 Nikolay Govorov */}}
+{{/* SPDX-License-Identifier: AGPL-3.0-or-later */}}
+{{define "layout"}}
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>{{template "title" .}} — Mirum</title>
+ </head>
+ <body>
+ {{template "content" .}}
+ </body>
+</html>
+{{end}}
diff --git a/cmd/mirumd/templates/login.html b/cmd/mirumd/templates/login.html
new file mode 100644
--- /dev/null
+++ b/cmd/mirumd/templates/login.html
@@ -0,0 +1,21 @@
+{{/* Copyright (c) 2026 Nikolay Govorov */}}
+{{/* SPDX-License-Identifier: AGPL-3.0-or-later */}}
+{{define "title"}}Login{{end}}
+
+{{define "content"}}
+<h1>Login</h1>
+
+{{if .Error}}
+<p style="color:red">{{.Error}}</p>
+{{end}}
+
+<form method="POST" action="/auth/login">
+ <input type="hidden" name="csrf" value="{{.CSRF}}">
+ <label>Email<br><input type="email" name="email" required></label>
+ <br><br>
+ <label>Password<br><input type="password" name="password" required></label>
+ <br><br>
+ <button type="submit">Login</button>
+</form>
+
+{{end}}
diff --git a/internal/database/database.go b/internal/database/database.go
index 1f88692..6ae3cd2 100644
--- a/internal/database/database.go
+++ b/internal/database/database.go
@@ -8,9 +8,12 @@ import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
+ "crypto/subtle"
"encoding/base64"
"errors"
"fmt"
+ "strings"
+ "time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/tern/v2/migrate"
@@ -26,13 +29,14 @@ const (
)
var (
- errOpen = errors.New("database: failed to open")
- errPing = errors.New("database: failed to ping")
- errAcquire = errors.New("database: failed to acquire connection")
- errMigrate = errors.New("database: failed to create migrator")
- errCreateUser = errors.New("database: failed to create user")
- errSetPassword = errors.New("database: failed to set password")
- errDeleteUser = errors.New("database: failed to delete user")
+ errOpen = errors.New("database: failed to open")
+ errPing = errors.New("database: failed to ping")
+ errAcquire = errors.New("database: failed to acquire connection")
+ errMigrate = errors.New("database: failed to create migrator")
+ errCreateUser = errors.New("database: failed to create user")
+ errSetPassword = errors.New("database: failed to set password")
+ errDeleteUser = errors.New("database: failed to delete user")
+ errInvalidCreds = errors.New("invalid credentials")
)
// DB wraps a pgx connection pool.
@@ -82,9 +86,91 @@ func (db *DB) Migrate(ctx context.Context) error {
`DROP TABLE users`,
)
+ migrator.AppendMigration("create_sessions",
+ `CREATE TABLE sessions (
+ token TEXT PRIMARY KEY,
+ user_id UUID NOT NULL REFERENCES users(id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ expires_at TIMESTAMPTZ NOT NULL
+ );
+ CREATE INDEX sessions_expires_at ON sessions (expires_at)`,
+ `DROP TABLE sessions`,
+ )
+
return migrator.Migrate(ctx)
}
+const SessionTTL = 14 * 24 * time.Hour // 14 days
+
+// hashToken returns the hex-encoded SHA-256 of a session token.
+func hashToken(token string) string {
+ h := sha256.Sum256([]byte(token))
+ return fmt.Sprintf("%x", h)
+}
+
+// CreateSession generates a random token, stores its hash, and returns the token.
+func (db *DB) CreateSession(ctx context.Context, userID string) (string, error) {
+ b := make([]byte, 32)
+ if _, err := rand.Read(b); err != nil {
+ return "", err
+ }
+ token := base64.RawURLEncoding.EncodeToString(b)
+
+ _, err := db.Pool.Exec(ctx,
+ `INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, now() + $3)`,
+ hashToken(token), userID, SessionTTL,
+ )
+ if err != nil {
+ return "", err
+ }
+ return token, nil
+}
+
+// Session holds info about an authenticated session.
+type Session struct {
+ UserID string
+ Email string
+}
+
+// GetSession returns session info for a valid, non-expired session.
+// It extends the session expiry only when less than half the TTL remains,
+// avoiding a write on every request.
+func (db *DB) GetSession(ctx context.Context, token string) (*Session, error) {
+ h := hashToken(token)
+ var s Session
+ var expiresAt time.Time
+ err := db.Pool.QueryRow(ctx,
+ `SELECT s.user_id, u.email, s.expires_at
+ FROM sessions s JOIN users u ON u.id = s.user_id
+ WHERE s.token = $1 AND s.expires_at > now() AND u.deleted_at IS NULL`,
+ h,
+ ).Scan(&s.UserID, &s.Email, &expiresAt)
+ if err != nil {
+ return nil, err
+ }
+
+ if time.Until(expiresAt) < SessionTTL/2 {
+ db.Pool.Exec(ctx,
+ `UPDATE sessions SET expires_at = now() + $2 WHERE token = $1`,
+ h, SessionTTL,
+ )
+ }
+
+ return &s, nil
+}
+
+// DeleteSession removes a session (logout).
+func (db *DB) DeleteSession(ctx context.Context, token string) error {
+ _, err := db.Pool.Exec(ctx, `DELETE FROM sessions WHERE token = $1`, hashToken(token))
+ return err
+}
+
+// PurgeExpiredSessions deletes all expired sessions.
+func (db *DB) PurgeExpiredSessions(ctx context.Context) error {
+ _, err := db.Pool.Exec(ctx, `DELETE FROM sessions WHERE expires_at < now()`)
+ return err
+}
+
// CreateUser hashes the password with argon2id and inserts a new user.
// The pepper is a server-side secret not stored in the database.
func (db *DB) CreateUser(ctx context.Context, email, password string, pepper []byte) (string, error) {
@@ -105,40 +191,111 @@ func (db *DB) CreateUser(ctx context.Context, email, password string, pepper []b
return id, nil
}
-// SetPassword updates the password for a user identified by email.
+// SetPassword updates the password and invalidates all existing sessions.
func (db *DB) SetPassword(ctx context.Context, email, password string, pepper []byte) error {
hash, err := hashPassword(password, pepper)
if err != nil {
return err
}
- tag, err := db.Pool.Exec(ctx,
- `UPDATE users SET password = $1 WHERE email = $2`,
+ tx, err := db.Pool.Begin(ctx)
+ if err != nil {
+ return errors.Join(errSetPassword, err)
+ }
+ defer tx.Rollback(ctx)
+
+ var id string
+ err = tx.QueryRow(ctx,
+ `UPDATE users SET password = $1 WHERE email = $2 AND deleted_at IS NULL RETURNING id`,
hash, email,
- )
+ ).Scan(&id)
if err != nil {
return errors.Join(errSetPassword, err)
}
- if tag.RowsAffected() == 0 {
- return fmt.Errorf("user not found: %s", email)
+
+ _, err = tx.Exec(ctx, `DELETE FROM sessions WHERE user_id = $1`, id)
+ if err != nil {
+ return errors.Join(errSetPassword, err)
}
- return nil
+ return tx.Commit(ctx)
}
// DeleteUser clears all fields but keeps the row to preserve the id.
+// Also deletes all sessions for that user in a single transaction.
func (db *DB) DeleteUser(ctx context.Context, email string) error {
- tag, err := db.Pool.Exec(ctx,
- `UPDATE users SET email = '<invalid>', password = '<invalid>', deleted_at = now() WHERE email = $1`,
+ tx, err := db.Pool.Begin(ctx)
+ if err != nil {
+ return errors.Join(errDeleteUser, err)
+ }
+ defer tx.Rollback(ctx)
+
+ var id string
+ err = tx.QueryRow(ctx,
+ `UPDATE users SET email = id::text, password = '', deleted_at = now() WHERE email = $1 RETURNING id`,
email,
- )
+ ).Scan(&id)
if err != nil {
return errors.Join(errDeleteUser, err)
}
- if tag.RowsAffected() == 0 {
- return fmt.Errorf("user not found: %s", email)
+
+ _, err = tx.Exec(ctx, `DELETE FROM sessions WHERE user_id = $1`, id)
+ if err != nil {
+ return errors.Join(errDeleteUser, err)
+ }
+
+ return tx.Commit(ctx)
+}
+
+// VerifyPassword checks credentials and returns the user ID.
+func (db *DB) VerifyPassword(ctx context.Context, email, password string, pepper []byte) (string, error) {
+ var id, hash string
+ err := db.Pool.QueryRow(ctx,
+ `SELECT id, password FROM users WHERE email = $1 AND deleted_at IS NULL`,
+ email,
+ ).Scan(&id, &hash)
+ if err != nil {
+ return "", errInvalidCreds
+ }
+
+ if !verifyHash(password, hash, pepper) {
+ return "", errInvalidCreds
}
- return nil
+
+ return id, nil
+}
+
+// verifyHash parses a PHC-format argon2id string and compares.
+// Format: $argon2id$v=19$m=65536,t=3,p=2$<salt>$<key>
+func verifyHash(password, encoded string, pepper []byte) bool {
+ // $argon2id$v=19$m=65536,t=3,p=2$salt$key → 6 parts
+ parts := strings.Split(encoded, "$")
+ if len(parts) != 6 || parts[1] != "argon2id" {
+ return false
+ }
+
+ var memory, time uint32
+ var threads uint8
+ if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil {
+ return false
+ }
+
+ salt, err := base64.RawStdEncoding.DecodeString(parts[4])
+ if err != nil {
+ return false
+ }
+ expectedKey, err := base64.RawStdEncoding.DecodeString(parts[5])
+ if err != nil {
+ return false
+ }
+
+ mac := hmac.New(sha256.New, pepper)
+ mac.Write([]byte(password))
+ peppered := mac.Sum(nil)
+
+ key := argon2.IDKey(peppered, salt, time, memory, threads, uint32(len(expectedKey)))
+
+ return subtle.ConstantTimeCompare(key, expectedKey) == 1
}
// hashPassword produces a PHC-format string:
diff --git a/pkg/mirumd.service b/pkg/mirumd.service
index e470c99..ab46526 100644
--- a/pkg/mirumd.service
+++ b/pkg/mirumd.service
@@ -4,7 +4,7 @@
[Unit]
Description=Mirum daemon (modern CI platform)
Requires=network-online.target
-After=time-sync.target network-online.target remote-fs.target nss-lookup.target
+After=time-sync.target network-online.target remote-fs.target nss-lookup.target postgresql.service
Wants=time-sync.target
# Socket activation (optional):
@@ -29,7 +29,7 @@ RestartSec=30
WatchdogSec=30
NotifyAccess=main
ExecPaths=/usr/local/bin/mirumd /usr/lib
-ExecStart=/usr/local/bin/mirumd --config=/etc/mirum/mirumd.yaml
+ExecStart=/usr/local/bin/mirumd daemon --config=/etc/mirum/mirumd.yaml
LimitCORE=infinity
LimitNOFILE=500000
AmbientCapabilities=CAP_NET_BIND_SERVICE
diff --git a/pkg/mirumd.yaml b/pkg/mirumd.yaml
index f140100..63d7d72 100644
--- a/pkg/mirumd.yaml
+++ b/pkg/mirumd.yaml
@@ -5,9 +5,9 @@
# See mirumd.socket for details (FileDescriptorName=grpc / web).
grpc_addr: :2026
www_addr: :3000
-secret: ""
+worker_secret: ""
+webhook_secret: ""
token: ""
pepper: ""
-dsn: ""
+database_uri: ""
admin_socket: /run/mirum/admin.sock
-script: .mirum/main.star