Skip to content

Commit 47cb485

Browse files
committed
fix: harden runtime daemon
1 parent 64a236a commit 47cb485

17 files changed

Lines changed: 351 additions & 30 deletions

File tree

apps/druid/adapters/cli/daemon.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package cli
22

33
import (
4+
"fmt"
45
"net"
56
"os"
67
"path/filepath"
8+
"strconv"
79
"strings"
10+
"time"
811

912
"github.com/gofiber/fiber/v2"
1013
runtimehandlers "github.com/highcard-dev/daemon/apps/druid/adapters/http/handlers"
@@ -37,6 +40,9 @@ var k8sKubeconfig string
3740
var runtimeListen string
3841
var runtimePublicListen string
3942
var runtimeInternalToken string
43+
var runtimeAllowUnauthenticatedPublic bool
44+
var runtimeAllowUnauthenticatedManagement bool
45+
var runtimeWorkerTimeout time.Duration
4046
var runtimeWorkerCallbackListen string
4147
var runtimeWorkerCallbackURL string
4248
var runtimeWorkerDaemonURL string
@@ -65,6 +71,9 @@ func init() {
6571
DaemonCommand.Flags().StringVar(&runtimeListen, "listen", "", "Optional management HTTP listen address, for example :8081")
6672
DaemonCommand.Flags().StringVar(&runtimePublicListen, "public-listen", "", "Optional public dashboard HTTP listen address, for example :8082")
6773
DaemonCommand.Flags().StringVar(&runtimeInternalToken, "internal-token", "", "Optional bearer token required for management HTTP API requests")
74+
DaemonCommand.Flags().BoolVar(&runtimeAllowUnauthenticatedPublic, "unsafe-allow-unauthenticated-public", false, "Allow unauthenticated public HTTP routes without --auth-jwks-url")
75+
DaemonCommand.Flags().BoolVar(&runtimeAllowUnauthenticatedManagement, "unsafe-allow-unauthenticated-management", false, "Allow unauthenticated management HTTP routes without --internal-token")
76+
DaemonCommand.Flags().DurationVar(&runtimeWorkerTimeout, "worker-timeout", 20*time.Minute, "Maximum time for runtime materialization workers")
6877
DaemonCommand.Flags().StringVar(&runtimeWorkerCallbackListen, "worker-callback-listen", "", "Optional internal worker callback listen address, for example :8083")
6978
DaemonCommand.Flags().StringVar(&runtimeWorkerCallbackURL, "worker-callback-url", "", "URL workers use to call back to this daemon")
7079
DaemonCommand.Flags().StringVar(&runtimeWorkerDaemonURL, "worker-daemon-url", "", "URL dev workers use for daemon management API calls")
@@ -93,6 +102,10 @@ func init() {
93102
}
94103

95104
func runRuntimeDaemon() error {
105+
loadRuntimeDaemonEnv()
106+
if err := validateRuntimeDaemonAuthConfig(); err != nil {
107+
return err
108+
}
96109
kubernetesConfig := runtimekubernetes.Config{
97110
Namespace: k8sNamespace,
98111
StorageClass: k8sStorageClass,
@@ -117,7 +130,7 @@ func runRuntimeDaemon() error {
117130
manager := services.NewRuntimeScrollManager(runtime.Store)
118131
supervisor := appservices.NewRuntimeSupervisor(runtime.Store, manager, runtime.Backend)
119132
callbacks := appservices.NewWorkerCallbackManager()
120-
loadRuntimeDaemonEnv()
133+
supervisor.SetWorkerTimeout(runtimeWorkerTimeout)
121134
callbackConfig := ports.RuntimeWorkerCallbackConfig{
122135
Listen: runtimeWorkerCallbackListen,
123136
URL: runtimeWorkerCallbackURL,
@@ -153,9 +166,11 @@ func runRuntimeDaemon() error {
153166
return err
154167
}
155168
scrollHandler := runtimehandlers.NewScrollHandler(supervisor, consoleService, logManager, authorizer)
169+
scrollHandler.SetAllowUnauthenticatedPublic(runtimeAllowUnauthenticatedPublic)
156170
websocketHandler := runtimehandlers.NewWebsocketHandler(consoleService)
157171
websocketHandler.SetScrollHandler(scrollHandler)
158172
websocketHandler.SetAuthorizer(authorizer)
173+
websocketHandler.SetAllowUnauthenticatedPublic(runtimeAllowUnauthenticatedPublic)
159174
handlers := runtimehandlers.RouteHandlers{
160175
Server: runtimehandlers.NewRuntimeServer(
161176
runtimehandlers.NewHealthHandler(),
@@ -218,6 +233,42 @@ func loadRuntimeDaemonEnv() {
218233
if runtimeInternalToken == "" {
219234
runtimeInternalToken = os.Getenv("DRUID_INTERNAL_TOKEN")
220235
}
236+
if !runtimeAllowUnauthenticatedPublic {
237+
runtimeAllowUnauthenticatedPublic = envBool("DRUID_UNSAFE_ALLOW_UNAUTHENTICATED_PUBLIC")
238+
}
239+
if !runtimeAllowUnauthenticatedManagement {
240+
runtimeAllowUnauthenticatedManagement = envBool("DRUID_UNSAFE_ALLOW_UNAUTHENTICATED_MANAGEMENT")
241+
}
242+
if runtimeWorkerTimeout == 0 {
243+
runtimeWorkerTimeout = 20 * time.Minute
244+
}
245+
if raw := strings.TrimSpace(os.Getenv("DRUID_WORKER_TIMEOUT")); raw != "" {
246+
if parsed, err := time.ParseDuration(raw); err == nil {
247+
runtimeWorkerTimeout = parsed
248+
}
249+
}
250+
}
251+
252+
func envBool(name string) bool {
253+
value := strings.TrimSpace(os.Getenv(name))
254+
if value == "" {
255+
return false
256+
}
257+
parsed, err := strconv.ParseBool(value)
258+
return err == nil && parsed
259+
}
260+
261+
func validateRuntimeDaemonAuthConfig() error {
262+
if runtimePublicListen != "" && runtimeAuthJWKSURL == "" && !runtimeAllowUnauthenticatedPublic {
263+
return fmt.Errorf("public listener %s requires --auth-jwks-url or --unsafe-allow-unauthenticated-public", runtimePublicListen)
264+
}
265+
if runtimeListen != "" && runtimeInternalToken == "" && !runtimeAllowUnauthenticatedManagement {
266+
return fmt.Errorf("management listener %s requires --internal-token or --unsafe-allow-unauthenticated-management", runtimeListen)
267+
}
268+
if runtimeWorkerTimeout <= 0 {
269+
return fmt.Errorf("worker timeout must be greater than zero")
270+
}
271+
return nil
221272
}
222273

223274
func openWorkerCallbackListener(listen string) (net.Listener, error) {

apps/druid/adapters/cli/worker_pull.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"strings"
11+
"time"
1112

1213
"github.com/highcard-dev/daemon/internal/callbackapi"
1314
"github.com/highcard-dev/daemon/internal/core/domain"
@@ -309,7 +310,9 @@ func reportWorkerResult(action ports.RuntimeWorkerAction, result ports.RuntimeWo
309310
ScrollYaml: workerString(result.ScrollYAML),
310311
Token: action.CallbackToken,
311312
}
312-
res, err := client.CompleteWorkerWithResponse(context.Background(), action.RuntimeID, body)
313+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
314+
defer cancel()
315+
res, err := client.CompleteWorkerWithResponse(ctx, action.RuntimeID, body)
313316
if err != nil {
314317
return err
315318
}

apps/druid/adapters/http/handlers/auth.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ const ownerLocal = "druid-owner-id"
1010

1111
func (h *ScrollHandler) PublicAuth(c *fiber.Ctx) error {
1212
if h.authorizer == nil {
13-
return c.Next()
13+
if h.allowUnauthenticatedPublic {
14+
return c.Next()
15+
}
16+
return fiber.NewError(fiber.StatusUnauthorized, "public authentication is not configured")
1417
}
1518
auth, err := h.authorizer.CheckHeader(c)
1619
if err != nil {
1720
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
1821
}
1922
if auth == nil {
20-
return c.Next()
23+
if h.allowUnauthenticatedPublic {
24+
return c.Next()
25+
}
26+
return fiber.NewError(fiber.StatusUnauthorized, "missing authorization token")
2127
}
2228
c.Locals(ownerLocal, auth.Subject)
2329
id := c.Params("id")
@@ -46,12 +52,13 @@ func (h *ScrollHandler) authorizeRuntimeOwner(id string, subject string) error {
4652

4753
func (h *WebsocketHandler) PublicQueryAuth(c *websocket.Conn) bool {
4854
if h.authorizer == nil {
49-
return true
55+
return h.allowUnauthenticatedPublic
5056
}
51-
if _, err := h.authorizer.CheckQuery(c.Params("id"), c.Query("token")); err != nil {
57+
auth, err := h.authorizer.CheckQuery(c.Params("id"), c.Query("token"))
58+
if err != nil {
5259
return false
5360
}
54-
return true
61+
return auth != nil || h.allowUnauthenticatedPublic
5562
}
5663

5764
type jwksProvider interface {

apps/druid/adapters/http/handlers/scroll_handler.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import (
1313
)
1414

1515
type ScrollHandler struct {
16-
supervisor *appservices.RuntimeSupervisor
17-
consoleService *services.ConsoleManager
18-
logService *services.LogManager
19-
authorizer ports.AuthorizerServiceInterface
16+
supervisor *appservices.RuntimeSupervisor
17+
consoleService *services.ConsoleManager
18+
logService *services.LogManager
19+
authorizer ports.AuthorizerServiceInterface
20+
allowUnauthenticatedPublic bool
2021
}
2122

2223
func NewScrollHandler(supervisor *appservices.RuntimeSupervisor, consoleService *services.ConsoleManager, logService *services.LogManager, authorizer ...ports.AuthorizerServiceInterface) *ScrollHandler {
@@ -32,6 +33,10 @@ func NewScrollHandler(supervisor *appservices.RuntimeSupervisor, consoleService
3233
}
3334
}
3435

36+
func (h *ScrollHandler) SetAllowUnauthenticatedPublic(allow bool) {
37+
h.allowUnauthenticatedPublic = allow
38+
}
39+
3540
func registryCredentials(in *[]api.RegistryCredential) []domain.RegistryCredential {
3641
if in == nil || len(*in) == 0 {
3742
return nil
@@ -180,7 +185,7 @@ func (h *ScrollHandler) RunScrollCommand(c *fiber.Ctx, id string, command string
180185
if err != nil {
181186
return err
182187
}
183-
updated, err := h.supervisor.Run(runtimeScroll.ID, command)
188+
updated, err := h.supervisor.RunWithContext(c.UserContext(), runtimeScroll.ID, command)
184189
if err != nil {
185190
return err
186191
}
@@ -256,7 +261,7 @@ func (h *ScrollHandler) RunDaemonCommand(c *fiber.Ctx) error {
256261
if request.Command == "" {
257262
return fiber.NewError(fiber.StatusBadRequest, "command is required")
258263
}
259-
if _, err := h.supervisor.Run(c.Params("id"), request.Command); err != nil {
264+
if _, err := h.supervisor.RunWithContext(c.UserContext(), c.Params("id"), request.Command); err != nil {
260265
return err
261266
}
262267
return c.SendStatus(fiber.StatusOK)

apps/druid/adapters/http/handlers/websocket_handler.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111
)
1212

1313
type WebsocketHandler struct {
14-
consoleService *services.ConsoleManager
15-
scrolls *ScrollHandler
16-
authorizer ports.AuthorizerServiceInterface
14+
consoleService *services.ConsoleManager
15+
scrolls *ScrollHandler
16+
authorizer ports.AuthorizerServiceInterface
17+
allowUnauthenticatedPublic bool
1718
}
1819

1920
func NewWebsocketHandler(consoleService *services.ConsoleManager) *WebsocketHandler {
@@ -28,6 +29,10 @@ func (h *WebsocketHandler) SetAuthorizer(authorizer ports.AuthorizerServiceInter
2829
h.authorizer = authorizer
2930
}
3031

32+
func (h *WebsocketHandler) SetAllowUnauthenticatedPublic(allow bool) {
33+
h.allowUnauthenticatedPublic = allow
34+
}
35+
3136
func (h *WebsocketHandler) AttachConsole(c *websocket.Conn) {
3237
consoleID := c.Params("console")
3338
if id := c.Params("id"); id != "" {

apps/druid/core/services/runtime_access.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import (
88
)
99

1010
func (s *RuntimeSupervisor) Run(id string, command string) (*domain.RuntimeScroll, error) {
11+
return s.RunWithContext(context.Background(), id, command)
12+
}
13+
14+
func (s *RuntimeSupervisor) RunWithContext(ctx context.Context, id string, command string) (*domain.RuntimeScroll, error) {
1115
session, err := s.sessionFor(id)
1216
if err != nil {
1317
return nil, err
1418
}
15-
return session.Run(command)
19+
return session.RunWithContext(ctx, command)
1620
}
1721

1822
func (s *RuntimeSupervisor) Ports(id string) ([]domain.RuntimePortStatus, error) {

0 commit comments

Comments
 (0)