Skip to content

Commit 9fb9a2d

Browse files
committed
fix(security): Dragonfly manager job API unauthenticated access
Dragonfly Manager's Job REST API endpoints lack authentication, allowing unauthenticated attackers to create, query, modify, and delete jobs, potentially leading to resource exhaustion, information disclosure, and service disruption. Signed-off-by: Gaius <gaius.qi@gmail.com>
1 parent a0240e2 commit 9fb9a2d

File tree

18 files changed

+148
-67
lines changed

18 files changed

+148
-67
lines changed

.github/workflows/compatibility-e2e.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ jobs:
3131
include:
3232
- module: manager
3333
image: manager
34-
image-tag: v2.4.1-beta.1
34+
image-tag: v2.4.1-rc.0
3535
chart-name: manager
3636
skip: "Rate Limit"
3737
- module: scheduler
3838
image: scheduler
39-
image-tag: v2.4.1-beta.1
39+
image-tag: v2.4.1-rc.0
4040
chart-name: scheduler
4141
skip: "Rate Limit"
4242
- module: client
@@ -137,12 +137,17 @@ jobs:
137137
kind load docker-image dragonflyoss/client:latest
138138
kind load docker-image dragonflyoss/dfinit:latest
139139
140+
- name: Generate Dragonfly PAT
141+
run: |
142+
DRAGONFLY_PAT=$(uuidgen | tr -d '\n' | base64 | tr '+/' '-_' | tr -d '=')
143+
echo "DRAGONFLY_PAT=$DRAGONFLY_PAT" >> $GITHUB_ENV
144+
140145
- name: Setup Helm
141146
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
142147

143148
- name: Setup dragonfly
144149
run: |
145-
helm install --wait --timeout 15m --dependency-update --create-namespace --namespace dragonfly-system --set ${{ matrix.chart-name }}.image.tag=${{ matrix.image-tag }} --set ${{ matrix.chart-name }}.image.repository=dragonflyoss/${{ matrix.image }} -f ${{ env.DRAGONFLY_CHARTS_CONFIG_PATH }} dragonfly ${{ env.DRAGONFLY_CHARTS_PATH }}
150+
helm install --wait --timeout 15m --dependency-update --create-namespace --namespace dragonfly-system --set ${{ matrix.chart-name }}.image.tag=${{ matrix.image-tag }} --set ${{ matrix.chart-name }}.image.repository=dragonflyoss/${{ matrix.image }} --set manager.extraEnvVars[0].name=DRAGONFLY_PAT --set manager.extraEnvVars[0].value=${{ env.DRAGONFLY_PAT }} -f ${{ env.DRAGONFLY_CHARTS_CONFIG_PATH }} dragonfly ${{ env.DRAGONFLY_CHARTS_PATH }}
146151
mkdir -p /tmp/artifact/dufs && chmod 777 /tmp/artifact/dufs
147152
kubectl apply -f ${{ env.DRAGONFLY_FILE_SERVER_PATH }}
148153
kubectl wait po dufs-0 --namespace dragonfly-e2e --for=condition=ready --timeout=10m

.github/workflows/e2e-rate-limit.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,17 @@ jobs:
120120
kind load docker-image dragonflyoss/client:latest
121121
kind load docker-image dragonflyoss/dfinit:latest
122122
123+
- name: Generate Dragonfly PAT
124+
run: |
125+
DRAGONFLY_PAT=$(uuidgen | tr -d '\n' | base64 | tr '+/' '-_' | tr -d '=')
126+
echo "DRAGONFLY_PAT=$DRAGONFLY_PAT" >> $GITHUB_ENV
127+
123128
- name: Setup Helm
124129
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
125130

126131
- name: Setup dragonfly
127132
run: |
128-
helm install --wait --timeout 15m --dependency-update --create-namespace --namespace dragonfly-system -f ${{ matrix.charts-config }} dragonfly ${{ env.DRAGONFLY_CHARTS_PATH }}
133+
helm install --wait --timeout 15m --dependency-update --create-namespace --namespace dragonfly-system --set manager.extraEnvVars[0].name=DRAGONFLY_PAT --set manager.extraEnvVars[0].value=${{ env.DRAGONFLY_PAT }} -f ${{ matrix.charts-config }} dragonfly ${{ env.DRAGONFLY_CHARTS_PATH }}
129134
mkdir -p /tmp/artifact/dufs && chmod 777 /tmp/artifact/dufs
130135
kubectl apply -f ${{ env.DRAGONFLY_FILE_SERVER_PATH }}
131136
kubectl wait po dufs-0 --namespace dragonfly-e2e --for=condition=ready --timeout=10m

.github/workflows/e2e.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,17 @@ jobs:
124124
kind load docker-image dragonflyoss/client:latest
125125
kind load docker-image dragonflyoss/dfinit:latest
126126
127+
- name: Generate Dragonfly PAT
128+
run: |
129+
DRAGONFLY_PAT=$(uuidgen | tr -d '\n' | base64 | tr '+/' '-_' | tr -d '=')
130+
echo "DRAGONFLY_PAT=$DRAGONFLY_PAT" >> $GITHUB_ENV
131+
127132
- name: Setup Helm
128133
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
129134

130135
- name: Setup dragonfly
131136
run: |
132-
helm install --wait --timeout 15m --dependency-update --create-namespace --namespace dragonfly-system -f ${{ matrix.charts-config }} dragonfly ${{ env.DRAGONFLY_CHARTS_PATH }}
137+
helm install --wait --timeout 15m --dependency-update --create-namespace --namespace dragonfly-system --set manager.extraEnvVars[0].name=DRAGONFLY_PAT --set manager.extraEnvVars[0].value=${{ env.DRAGONFLY_PAT }} -f ${{ matrix.charts-config }} dragonfly ${{ env.DRAGONFLY_CHARTS_PATH }}
133138
mkdir -p /tmp/artifact/dufs && chmod 777 /tmp/artifact/dufs
134139
kubectl apply -f ${{ env.DRAGONFLY_FILE_SERVER_PATH }}
135140
kubectl wait po dufs-0 --namespace dragonfly-e2e --for=condition=ready --timeout=10m

manager/database/database.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"encoding/json"
2121
"errors"
2222
"fmt"
23+
"os"
24+
"time"
2325

2426
caches "github.com/go-gorm/caches/v4"
2527
"github.com/redis/go-redis/v9"
@@ -38,6 +40,11 @@ const (
3840
DefaultClusterName = "cluster-1"
3941
)
4042

43+
const (
44+
// DragonflyPATEnvName is the environment variable name for generating personal access token.
45+
DragonflyPATEnvName = "DRAGONFLY_PAT"
46+
)
47+
4148
type Database struct {
4249
DB *gorm.DB
4350
RDB redis.UniversalClient
@@ -230,5 +237,19 @@ func seed(db *gorm.DB) error {
230237
}
231238
}
232239

240+
// If DRAGONFLY_PAT is set, create a default personal access token for user ID 1(root user).
241+
if pat := os.Getenv(DragonflyPATEnvName); pat != "" {
242+
if err := db.Model(models.PersonalAccessToken{}).Create(&models.PersonalAccessToken{
243+
Name: "default",
244+
Token: pat,
245+
Scopes: types.DefaultPersonalAccessTokenScopes,
246+
State: models.PersonalAccessTokenStateActive,
247+
ExpiredAt: time.Now().AddDate(10, 0, 0),
248+
UserID: 1,
249+
}).Error; err != nil {
250+
return err
251+
}
252+
}
253+
233254
return nil
234255
}

manager/router/router.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,8 @@ func Init(cfg *config.Config, logDir string, service service.Service, database *
201201
config.GET(":id", h.GetConfig)
202202
config.GET("", h.GetConfigs)
203203

204-
// TODO Add auth to the following routes and fix the tests.
205204
// Job.
206-
job := apiv1.Group("/jobs")
205+
job := apiv1.Group("/jobs", jwt.MiddlewareFunc(), rbac)
207206
job.POST("", middlewares.CreateJobRateLimiter(limiter), h.CreateJob)
208207
job.DELETE(":id", h.DestroyJob)
209208
job.PATCH(":id", h.UpdateJob)

manager/service/personal_access_token.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ package service
1818

1919
import (
2020
"context"
21-
"encoding/base64"
22-
23-
"github.com/google/uuid"
2421

2522
"d7y.io/dragonfly/v2/manager/models"
2623
"d7y.io/dragonfly/v2/manager/types"
24+
"d7y.io/dragonfly/v2/pkg/auth"
2725
)
2826

2927
func (s *service) CreatePersonalAccessToken(ctx context.Context, json types.CreatePersonalAccessTokenRequest) (*models.PersonalAccessToken, error) {
@@ -34,7 +32,7 @@ func (s *service) CreatePersonalAccessToken(ctx context.Context, json types.Crea
3432
personalAccessToken := models.PersonalAccessToken{
3533
Name: json.Name,
3634
BIO: json.BIO,
37-
Token: s.generatePersonalAccessToken(),
35+
Token: auth.GeneratePersonalAccessToken(),
3836
Scopes: json.Scopes,
3937
State: models.PersonalAccessTokenStateActive,
4038
ExpiredAt: json.ExpiredAt,
@@ -101,7 +99,3 @@ func (s *service) GetPersonalAccessTokens(ctx context.Context, q types.GetPerson
10199

102100
return personalAccessToken, count, nil
103101
}
104-
105-
func (s *service) generatePersonalAccessToken() string {
106-
return base64.RawURLEncoding.EncodeToString([]byte(uuid.NewString()))
107-
}

pkg/auth/personal_access_token.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2026 The Dragonfly Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package auth
18+
19+
import (
20+
"encoding/base64"
21+
22+
"github.com/google/uuid"
23+
)
24+
25+
// GeneratePersonalAccessToken generates a new personal access token.
26+
func GeneratePersonalAccessToken() string {
27+
return base64.RawURLEncoding.EncodeToString([]byte(uuid.NewString()))
28+
}

test/e2e/e2e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ var _ = AfterSuite(func() {
8585
if err := util.GetFileServer().Purge(); err != nil {
8686
fmt.Printf("failed to purge the e2e file server: %v\n", err)
8787
}
88-
8988
})
9089

9190
var _ = BeforeSuite(func() {
@@ -97,6 +96,7 @@ var _ = BeforeSuite(func() {
9796
Expect(err).NotTo(HaveOccurred())
9897
gitCommit := strings.Fields(string(rawGitCommit))[0]
9998
fmt.Printf("git commit: %s\n", gitCommit)
99+
100100
// Wait for peers to start and announce to scheduler.
101101
time.Sleep(5 * time.Minute)
102102
})

0 commit comments

Comments
 (0)