Skip to content

Commit ca83017

Browse files
authored
feat: accept provisioner keys for provisioner auth (#13972)
1 parent d488853 commit ca83017

File tree

9 files changed

+351
-42
lines changed

9 files changed

+351
-42
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ var (
245245
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
246246
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate},
247247
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
248+
rbac.ResourceProvisionerKeys.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
248249
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
249250
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
250251
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH},

coderd/httpmw/csrf.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler {
9393
return true
9494
}
9595

96+
if r.Header.Get(codersdk.ProvisionerDaemonKey) != "" {
97+
// If present, the provisioner daemon also is providing an api key
98+
// that will make them exempt from CSRF. But this is still useful
99+
// for enumerating the external auths.
100+
return true
101+
}
102+
96103
// If the X-CSRF-TOKEN header is set, we can exempt the func if it's valid.
97104
// This is the CSRF check.
98105
sent := r.Header.Get("X-CSRF-TOKEN")

coderd/httpmw/provisionerdaemon.go

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/coder/coder/v2/coderd/database"
99
"github.com/coder/coder/v2/coderd/database/dbauthz"
1010
"github.com/coder/coder/v2/coderd/httpapi"
11+
"github.com/coder/coder/v2/coderd/provisionerkey"
1112
"github.com/coder/coder/v2/codersdk"
1213
)
1314

@@ -19,11 +20,13 @@ func ProvisionerDaemonAuthenticated(r *http.Request) bool {
1920
}
2021

2122
type ExtractProvisionerAuthConfig struct {
22-
DB database.Store
23-
Optional bool
23+
DB database.Store
24+
Optional bool
25+
PSK string
26+
MultiOrgEnabled bool
2427
}
2528

26-
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, psk string) func(next http.Handler) http.Handler {
29+
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig) func(next http.Handler) http.Handler {
2730
return func(next http.Handler) http.Handler {
2831
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2932
ctx := r.Context()
@@ -36,37 +39,103 @@ func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, ps
3639
httpapi.Write(ctx, w, code, response)
3740
}
3841

39-
if psk == "" {
40-
// No psk means external provisioner daemons are not allowed.
41-
// So their auth is not valid.
42+
if !opts.MultiOrgEnabled {
43+
if opts.PSK == "" {
44+
handleOptional(http.StatusUnauthorized, codersdk.Response{
45+
Message: "External provisioner daemons not enabled",
46+
})
47+
return
48+
}
49+
50+
fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
51+
return
52+
}
53+
54+
psk := r.Header.Get(codersdk.ProvisionerDaemonPSK)
55+
key := r.Header.Get(codersdk.ProvisionerDaemonKey)
56+
if key == "" {
57+
if opts.PSK == "" {
58+
handleOptional(http.StatusUnauthorized, codersdk.Response{
59+
Message: "provisioner daemon key required",
60+
})
61+
return
62+
}
63+
64+
fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
65+
return
66+
}
67+
if psk != "" {
4268
handleOptional(http.StatusBadRequest, codersdk.Response{
43-
Message: "External provisioner daemons not enabled",
69+
Message: "provisioner daemon key and psk provided, but only one is allowed",
4470
})
4571
return
4672
}
4773

48-
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
49-
if token == "" {
74+
id, keyValue, err := provisionerkey.Parse(key)
75+
if err != nil {
5076
handleOptional(http.StatusUnauthorized, codersdk.Response{
51-
Message: "provisioner daemon auth token required",
77+
Message: "provisioner daemon key invalid",
78+
})
79+
return
80+
}
81+
82+
// nolint:gocritic // System must check if the provisioner key is valid.
83+
pk, err := opts.DB.GetProvisionerKeyByID(dbauthz.AsSystemRestricted(ctx), id)
84+
if err != nil {
85+
if httpapi.Is404Error(err) {
86+
handleOptional(http.StatusUnauthorized, codersdk.Response{
87+
Message: "provisioner daemon key invalid",
88+
})
89+
return
90+
}
91+
92+
handleOptional(http.StatusInternalServerError, codersdk.Response{
93+
Message: "get provisioner daemon key: " + err.Error(),
5294
})
5395
return
5496
}
5597

56-
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
98+
if provisionerkey.Compare(pk.HashedSecret, provisionerkey.HashSecret(keyValue)) {
5799
handleOptional(http.StatusUnauthorized, codersdk.Response{
58-
Message: "provisioner daemon auth token invalid",
100+
Message: "provisioner daemon key invalid",
59101
})
60102
return
61103
}
62104

63-
// The PSK does not indicate a specific provisioner daemon. So just
105+
// The provisioner key does not indicate a specific provisioner daemon. So just
64106
// store a boolean so the caller can check if the request is from an
65107
// authenticated provisioner daemon.
66108
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
109+
// store key used to authenticate the request
110+
ctx = context.WithValue(ctx, provisionerKeyAuthContextKey{}, pk)
67111
// nolint:gocritic // Authenticating as a provisioner daemon.
68112
ctx = dbauthz.AsProvisionerd(ctx)
69113
next.ServeHTTP(w, r.WithContext(ctx))
70114
})
71115
}
72116
}
117+
118+
type provisionerKeyAuthContextKey struct{}
119+
120+
func ProvisionerKeyAuthOptional(r *http.Request) (database.ProvisionerKey, bool) {
121+
user, ok := r.Context().Value(provisionerKeyAuthContextKey{}).(database.ProvisionerKey)
122+
return user, ok
123+
}
124+
125+
func fallbackToPSK(ctx context.Context, psk string, next http.Handler, w http.ResponseWriter, r *http.Request, handleOptional func(code int, response codersdk.Response)) {
126+
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
127+
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
128+
handleOptional(http.StatusUnauthorized, codersdk.Response{
129+
Message: "provisioner daemon psk invalid",
130+
})
131+
return
132+
}
133+
134+
// The PSK does not indicate a specific provisioner daemon. So just
135+
// store a boolean so the caller can check if the request is from an
136+
// authenticated provisioner daemon.
137+
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
138+
// nolint:gocritic // Authenticating as a provisioner daemon.
139+
ctx = dbauthz.AsProvisionerd(ctx)
140+
next.ServeHTTP(w, r.WithContext(ctx))
141+
}

coderd/provisionerkey/provisionerkey.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package provisionerkey
22

33
import (
44
"crypto/sha256"
5+
"crypto/subtle"
56
"fmt"
7+
"strings"
68

79
"github.com/google/uuid"
810
"golang.org/x/xerrors"
@@ -18,14 +20,37 @@ func New(organizationID uuid.UUID, name string) (database.InsertProvisionerKeyPa
1820
if err != nil {
1921
return database.InsertProvisionerKeyParams{}, "", xerrors.Errorf("generate token: %w", err)
2022
}
21-
hashedSecret := sha256.Sum256([]byte(secret))
23+
hashedSecret := HashSecret(secret)
2224
token := fmt.Sprintf("%s:%s", id, secret)
2325

2426
return database.InsertProvisionerKeyParams{
2527
ID: id,
2628
CreatedAt: dbtime.Now(),
2729
OrganizationID: organizationID,
2830
Name: name,
29-
HashedSecret: hashedSecret[:],
31+
HashedSecret: hashedSecret,
3032
}, token, nil
3133
}
34+
35+
func Parse(token string) (uuid.UUID, string, error) {
36+
parts := strings.Split(token, ":")
37+
if len(parts) != 2 {
38+
return uuid.UUID{}, "", xerrors.Errorf("invalid token format")
39+
}
40+
41+
id, err := uuid.Parse(parts[0])
42+
if err != nil {
43+
return uuid.UUID{}, "", xerrors.Errorf("parse id: %w", err)
44+
}
45+
46+
return id, parts[1], nil
47+
}
48+
49+
func HashSecret(secret string) []byte {
50+
h := sha256.Sum256([]byte(secret))
51+
return h[:]
52+
}
53+
54+
func Compare(a []byte, b []byte) bool {
55+
return subtle.ConstantTimeCompare(a, b) != 1
56+
}

codersdk/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ const (
7979
// ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon
8080
ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK"
8181

82+
// ProvisionerDaemonKey contains the authentication key for an external provisioner daemon
83+
ProvisionerDaemonKey = "Coder-Provisioner-Daemon-Key"
84+
8285
// BuildVersionHeader contains build information of Coder.
8386
BuildVersionHeader = "X-Coder-Build-Version"
8487

codersdk/provisionerdaemons.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ type ServeProvisionerDaemonRequest struct {
189189
Tags map[string]string `json:"tags"`
190190
// PreSharedKey is an authentication key to use on the API instead of the normal session token from the client.
191191
PreSharedKey string `json:"pre_shared_key"`
192+
// ProvisionerKey is an authentication key to use on the API instead of the normal session token from the client.
193+
ProvisionerKey string `json:"provisioner_key"`
192194
}
193195

194196
// ServeProvisionerDaemon returns the gRPC service for a provisioner daemon
@@ -223,8 +225,15 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
223225
headers := http.Header{}
224226

225227
headers.Set(BuildVersionHeader, buildinfo.Version())
226-
if req.PreSharedKey == "" {
227-
// use session token if we don't have a PSK.
228+
229+
if req.ProvisionerKey != "" {
230+
headers.Set(ProvisionerDaemonKey, req.ProvisionerKey)
231+
}
232+
if req.PreSharedKey != "" {
233+
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
234+
}
235+
if req.ProvisionerKey == "" && req.PreSharedKey == "" {
236+
// use session token if we don't have a PSK or provisioner key.
228237
jar, err := cookiejar.New(nil)
229238
if err != nil {
230239
return nil, xerrors.Errorf("create cookie jar: %w", err)
@@ -234,8 +243,6 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
234243
Value: c.SessionToken(),
235244
}})
236245
httpClient.Jar = jar
237-
} else {
238-
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
239246
}
240247

241248
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{

enterprise/coderd/coderd.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
110110
provisionerDaemonAuth: &provisionerDaemonAuth{
111111
psk: options.ProvisionerDaemonPSK,
112112
authorizer: options.Authorizer,
113+
db: options.Database,
113114
},
114115
}
115116
// This must happen before coderd initialization!
@@ -285,9 +286,11 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
285286
api.provisionerDaemonsEnabledMW,
286287
apiKeyMiddlewareOptional,
287288
httpmw.ExtractProvisionerDaemonAuthenticated(httpmw.ExtractProvisionerAuthConfig{
288-
DB: api.Database,
289-
Optional: true,
290-
}, api.ProvisionerDaemonPSK),
289+
DB: api.Database,
290+
Optional: true,
291+
PSK: api.ProvisionerDaemonPSK,
292+
MultiOrgEnabled: api.AGPL.Experiments.Enabled(codersdk.ExperimentMultiOrganization),
293+
}),
291294
// Either a user auth or provisioner auth is required
292295
// to move forward.
293296
httpmw.RequireAPIKeyOrProvisionerDaemonAuth(),

enterprise/coderd/provisionerdaemons.go

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -79,36 +79,58 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
7979

8080
type provisionerDaemonAuth struct {
8181
psk string
82+
db database.Store
8283
authorizer rbac.Authorizer
8384
}
8485

85-
// authorize returns mutated tags and true if the given HTTP request is authorized to access the provisioner daemon
86-
// protobuf API, and returns nil, false otherwise.
87-
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (map[string]string, bool) {
86+
// authorize returns mutated tags if the given HTTP request is authorized to access the provisioner daemon
87+
// protobuf API, and returns nil, err otherwise.
88+
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (map[string]string, error) {
8889
ctx := r.Context()
89-
apiKey, ok := httpmw.APIKeyOptional(r)
90-
if ok {
90+
apiKey, apiKeyOK := httpmw.APIKeyOptional(r)
91+
pk, pkOK := httpmw.ProvisionerKeyAuthOptional(r)
92+
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
93+
if !provAuth && !apiKeyOK {
94+
return nil, xerrors.New("no API key or provisioner key provided")
95+
}
96+
if apiKeyOK && pkOK {
97+
return nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.")
98+
}
99+
100+
if apiKeyOK {
91101
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
92102
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
93103
// Any authenticated user can create provisioner daemons scoped
94104
// for jobs that they own,
95-
return tags, true
105+
return tags, nil
96106
}
97107
ua := httpmw.UserAuthorization(r)
98-
if err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID)); err == nil {
99-
// User is allowed to create provisioner daemons
100-
return tags, true
108+
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
109+
if err != nil {
110+
if !provAuth {
111+
return nil, xerrors.New("user unauthorized")
112+
}
113+
114+
// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
115+
// This is to preserve backwards compatibility with existing user provisioner daemons.
116+
// If using PSK auth, the daemon is, by definition, scoped to the organization.
117+
tags = provisionersdk.MutateTags(uuid.Nil, tags)
118+
return tags, nil
101119
}
120+
121+
// User is allowed to create provisioner daemons
122+
return tags, nil
102123
}
103124

104-
// Check for PSK
105-
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
106-
if provAuth {
107-
// If using PSK auth, the daemon is, by definition, scoped to the organization.
108-
tags = provisionersdk.MutateTags(uuid.Nil, tags)
109-
return tags, true
125+
if pkOK {
126+
if pk.OrganizationID != orgID {
127+
return nil, xerrors.New("provisioner key unauthorized")
128+
}
110129
}
111-
return nil, false
130+
131+
// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
132+
tags = provisionersdk.MutateTags(uuid.Nil, tags)
133+
return tags, nil
112134
}
113135

114136
// Serves the provisioner daemon protobuf API over a WebSocket.
@@ -171,12 +193,13 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
171193
api.Logger.Warn(ctx, "unnamed provisioner daemon")
172194
}
173195

174-
tags, authorized := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
175-
if !authorized {
176-
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags))
196+
tags, err := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
197+
if err != nil {
198+
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags), slog.Error(err))
177199
httpapi.Write(ctx, rw, http.StatusForbidden,
178200
codersdk.Response{
179201
Message: fmt.Sprintf("You aren't allowed to create provisioner daemons with scope %q", tags[provisionersdk.TagScope]),
202+
Detail: err.Error(),
180203
},
181204
)
182205
return
@@ -209,7 +232,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
209232
)
210233

211234
authCtx := ctx
212-
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" {
235+
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" || r.Header.Get(codersdk.ProvisionerDaemonKey) != "" {
213236
//nolint:gocritic // PSK auth means no actor in request,
214237
// so use system restricted.
215238
authCtx = dbauthz.AsSystemRestricted(ctx)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy