Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ TINYAUTH_SERVER_PORT=3000
TINYAUTH_SERVER_ADDRESS="0.0.0.0"
# The path to the Unix socket.
TINYAUTH_SERVER_SOCKETPATH=
# Octal permission mode for the Unix socket (e.g. 0660). Left unset, the socket keeps the process umask's default.
TINYAUTH_SERVER_SOCKETMODE=

# auth config

Expand Down
43 changes: 43 additions & 0 deletions internal/bootstrap/router_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"net"
"net/http"
"os"
"runtime"
"strconv"
"time"

"github.com/tinyauthapp/tinyauth/internal/controller"
Expand Down Expand Up @@ -165,7 +167,36 @@ func (app *BootstrapApp) serveHTTP(ctx context.Context) error {
return app.serve(listener, server, ctx, "http")
}

// parseSocketMode parses an octal permission string such as "0660" into an os.FileMode.
func parseSocketMode(s string) (os.FileMode, error) {
v, err := strconv.ParseUint(s, 8, 32)

if err != nil || v > 0o777 {
return 0, fmt.Errorf("expected an octal mode such as 0660")
}

return os.FileMode(v), nil
}

func (app *BootstrapApp) serveUnix(ctx context.Context) error {
// Validate socketMode up front, before removing any existing socket, so a configuration error does
// not delete the current socket and then fail to start.
hasMode := app.config.Server.SocketMode != ""

var mode os.FileMode

if hasMode {
if runtime.GOOS == "windows" {
return errors.New("server.socketMode is not supported on Windows, where socket permissions cannot be enforced")
}

var perr error

if mode, perr = parseSocketMode(app.config.Server.SocketMode); perr != nil {
return fmt.Errorf("invalid server.socketMode %q: %w", app.config.Server.SocketMode, perr)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

_, err := os.Stat(app.config.Server.SocketPath)

if err == nil {
Expand All @@ -185,6 +216,18 @@ func (app *BootstrapApp) serveUnix(ctx context.Context) error {
return fmt.Errorf("failed to create unix socket listener: %w", err)
}

if hasMode {
// net.Listen creates the socket with the process umask's mode; chmod tightens it immediately.
// serve() has not started accepting connections yet, and connecting to the socket is not itself
// an auth bypass, so this is the standard Listen+Chmod pattern for Go unix sockets. It is
// preferred over changing the process-wide umask, which would race with any other file created
// during startup. Operators wanting a hard guarantee should also restrict the socket's directory.
if err := os.Chmod(app.config.Server.SocketPath, mode); err != nil {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
listener.Close()
return fmt.Errorf("failed to set unix socket mode: %w", err)
}
}

server := &http.Server{
Handler: app.router.Handler(),
}
Expand Down
55 changes: 55 additions & 0 deletions internal/bootstrap/router_bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package bootstrap

import (
"net"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseSocketMode(t *testing.T) {
ok := map[string]os.FileMode{
"0660": 0o660,
"660": 0o660,
"0600": 0o600,
"0": 0,
"0777": 0o777,
}

for in, want := range ok {
got, err := parseSocketMode(in)
assert.NoError(t, err, "input %q", in)
assert.Equal(t, want, got, "input %q", in)
}

for _, in := range []string{"", "abc", "0800", "999", "0x1a0", "1000"} {
_, err := parseSocketMode(in)
assert.Error(t, err, "input %q should be rejected", in)
}
}

// TestSocketModeAppliedToListener documents that os.Chmod on the listening socket path sets
// exactly the requested permission bits, which is what serveUnix relies on.
func TestSocketModeAppliedToListener(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("os.Chmod cannot set Unix permission bits on Windows (socketMode is rejected there)")
}

path := filepath.Join(t.TempDir(), "ta.sock")

listener, err := net.Listen("unix", path)
require.NoError(t, err)
defer listener.Close()

mode, err := parseSocketMode("0660")
require.NoError(t, err)
require.NoError(t, os.Chmod(path, mode))

info, err := os.Stat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o660), info.Mode().Perm())
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
1 change: 1 addition & 0 deletions internal/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ type ServerConfig struct {
Port int `description:"The port on which the server listens." yaml:"port,omitempty"`
Address string `description:"The address on which the server listens." yaml:"address,omitempty"`
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"`
SocketMode string `description:"Octal permission mode for the Unix socket (e.g. 0660). Left unset, the socket keeps the process umask's default." yaml:"socketMode,omitempty"`
}

type AuthConfig struct {
Expand Down