Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: accept provisioner keys for provisioner auth#13972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
f0ssel merged 10 commits intomainfromf0ssel/use-provisioner-key-auth
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes fromall 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
1 change: 1 addition & 0 deletionscoderd/database/dbauthz/dbauthz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -245,6 +245,7 @@ var (
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate},
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
rbac.ResourceProvisionerKeys.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH},
Expand Down
7 changes: 7 additions & 0 deletionscoderd/httpmw/csrf.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,13 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler {
returntrue
}

ifr.Header.Get(codersdk.ProvisionerDaemonKey)!="" {
// If present, the provisioner daemon also is providing an api key
// that will make them exempt from CSRF. But this is still useful
// for enumerating the external auths.
returntrue
}

// If the X-CSRF-TOKEN header is set, we can exempt the func if it's valid.
// This is the CSRF check.
sent:=r.Header.Get("X-CSRF-TOKEN")
Expand Down
95 changes: 82 additions & 13 deletionscoderd/httpmw/provisionerdaemon.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/provisionerkey"
"github.com/coder/coder/v2/codersdk"
)

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

type ExtractProvisionerAuthConfig struct {
DB database.Store
Optional bool
DB database.Store
Optional bool
PSK string
MultiOrgEnabled bool
}

func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, psk string) func(next http.Handler) http.Handler {
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand All@@ -36,37 +39,103 @@ func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, ps
httpapi.Write(ctx, w, code, response)
}

if psk == "" {
// No psk means external provisioner daemons are not allowed.
// So their auth is not valid.
if !opts.MultiOrgEnabled {
if opts.PSK == "" {
handleOptional(http.StatusUnauthorized, codersdk.Response{
Message: "External provisioner daemons not enabled",
})
return
}

fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
return
}

psk := r.Header.Get(codersdk.ProvisionerDaemonPSK)
key := r.Header.Get(codersdk.ProvisionerDaemonKey)
if key == "" {
if opts.PSK == "" {
handleOptional(http.StatusUnauthorized, codersdk.Response{
Message: "provisioner daemon key required",
})
return
}

fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
return
}
if psk != "" {
handleOptional(http.StatusBadRequest, codersdk.Response{
Message: "Externalprovisionerdaemons not enabled",
Message: "provisionerdaemon key and psk provided, but only one is allowed",
})
return
}

token:=r.Header.Get(codersdk.ProvisionerDaemonPSK)
iftoken == "" {
id, keyValue, err:=provisionerkey.Parse(key)
iferr != nil {
handleOptional(http.StatusUnauthorized, codersdk.Response{
Message: "provisioner daemon auth token required",
Message: "provisioner daemon key invalid",
})
return
}

// nolint:gocritic // System must check if the provisioner key is valid.
pk, err := opts.DB.GetProvisionerKeyByID(dbauthz.AsSystemRestricted(ctx), id)
if err != nil {
if httpapi.Is404Error(err) {
handleOptional(http.StatusUnauthorized, codersdk.Response{
Message: "provisioner daemon key invalid",
})
return
}

handleOptional(http.StatusInternalServerError, codersdk.Response{
Message: "get provisioner daemon key: " + err.Error(),
})
return
}

ifsubtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
ifprovisionerkey.Compare(pk.HashedSecret, provisionerkey.HashSecret(keyValue)) {
handleOptional(http.StatusUnauthorized, codersdk.Response{
Message: "provisioner daemonauth token invalid",
Message: "provisioner daemonkey invalid",
})
return
}

// ThePSK does not indicate a specific provisioner daemon. So just
// Theprovisioner key does not indicate a specific provisioner daemon. So just
// store a boolean so the caller can check if the request is from an
// authenticated provisioner daemon.
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
// store key used to authenticate the request
ctx = context.WithValue(ctx, provisionerKeyAuthContextKey{}, pk)
// nolint:gocritic // Authenticating as a provisioner daemon.
ctx = dbauthz.AsProvisionerd(ctx)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

type provisionerKeyAuthContextKey struct{}

func ProvisionerKeyAuthOptional(r *http.Request) (database.ProvisionerKey, bool) {
user, ok := r.Context().Value(provisionerKeyAuthContextKey{}).(database.ProvisionerKey)
return user, ok
}

func fallbackToPSK(ctx context.Context, psk string, next http.Handler, w http.ResponseWriter, r *http.Request, handleOptional func(code int, response codersdk.Response)) {
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
handleOptional(http.StatusUnauthorized, codersdk.Response{
Message: "provisioner daemon psk invalid",
})
return
}

// The PSK does not indicate a specific provisioner daemon. So just
// store a boolean so the caller can check if the request is from an
// authenticated provisioner daemon.
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
// nolint:gocritic // Authenticating as a provisioner daemon.
ctx = dbauthz.AsProvisionerd(ctx)
next.ServeHTTP(w, r.WithContext(ctx))
}
29 changes: 27 additions & 2 deletionscoderd/provisionerkey/provisionerkey.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,9 @@ package provisionerkey

import (
"crypto/sha256"
"crypto/subtle"
"fmt"
"strings"

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

return database.InsertProvisionerKeyParams{
ID: id,
CreatedAt: dbtime.Now(),
OrganizationID: organizationID,
Name: name,
HashedSecret: hashedSecret[:],
HashedSecret: hashedSecret,
}, token, nil
}

func Parse(token string) (uuid.UUID, string, error) {
parts := strings.Split(token, ":")
if len(parts) != 2 {
return uuid.UUID{}, "", xerrors.Errorf("invalid token format")
}

id, err := uuid.Parse(parts[0])
if err != nil {
return uuid.UUID{}, "", xerrors.Errorf("parse id: %w", err)
}

return id, parts[1], nil
}

func HashSecret(secret string) []byte {
h := sha256.Sum256([]byte(secret))
return h[:]
}

func Compare(a []byte, b []byte) bool {
return subtle.ConstantTimeCompare(a, b) != 1
}
3 changes: 3 additions & 0 deletionscodersdk/client.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,6 +79,9 @@ const (
// ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon
ProvisionerDaemonPSK="Coder-Provisioner-Daemon-PSK"

// ProvisionerDaemonKey contains the authentication key for an external provisioner daemon
ProvisionerDaemonKey="Coder-Provisioner-Daemon-Key"

// BuildVersionHeader contains build information of Coder.
BuildVersionHeader="X-Coder-Build-Version"

Expand Down
15 changes: 11 additions & 4 deletionscodersdk/provisionerdaemons.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -189,6 +189,8 @@ type ServeProvisionerDaemonRequest struct {
Tags map[string]string `json:"tags"`
// PreSharedKey is an authentication key to use on the API instead of the normal session token from the client.
PreSharedKey string `json:"pre_shared_key"`
// ProvisionerKey is an authentication key to use on the API instead of the normal session token from the client.
ProvisionerKey string `json:"provisioner_key"`
}

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

headers.Set(BuildVersionHeader, buildinfo.Version())
if req.PreSharedKey == "" {
// use session token if we don't have a PSK.

if req.ProvisionerKey != "" {
headers.Set(ProvisionerDaemonKey, req.ProvisionerKey)
}
if req.PreSharedKey != "" {
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
}
Comment on lines +229 to +234
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Should these be mutually exclusive?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I wanted it to fail at the API layer instead of silently taking one or the other. I could do a client error but thought it was cleaner to just have the server handle it.

if req.ProvisionerKey == "" && req.PreSharedKey == "" {
// use session token if we don't have a PSK or provisioner key.
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
Expand All@@ -234,8 +243,6 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
Value: c.SessionToken(),
}})
httpClient.Jar = jar
} else {
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
}

conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
Expand Down
9 changes: 6 additions & 3 deletionsenterprise/coderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,6 +109,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
provisionerDaemonAuth: &provisionerDaemonAuth{
psk: options.ProvisionerDaemonPSK,
authorizer: options.Authorizer,
db: options.Database,
},
}
// This must happen before coderd initialization!
Expand DownExpand Up@@ -284,9 +285,11 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
api.provisionerDaemonsEnabledMW,
apiKeyMiddlewareOptional,
httpmw.ExtractProvisionerDaemonAuthenticated(httpmw.ExtractProvisionerAuthConfig{
DB: api.Database,
Optional: true,
}, api.ProvisionerDaemonPSK),
DB: api.Database,
Optional: true,
PSK: api.ProvisionerDaemonPSK,
MultiOrgEnabled: api.AGPL.Experiments.Enabled(codersdk.ExperimentMultiOrganization),
}),
// Either a user auth or provisioner auth is required
// to move forward.
httpmw.RequireAPIKeyOrProvisionerDaemonAuth(),
Expand Down
63 changes: 43 additions & 20 deletionsenterprise/coderd/provisionerdaemons.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,36 +79,58 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {

type provisionerDaemonAuth struct {
psk string
db database.Store
authorizer rbac.Authorizer
}

// authorize returns mutated tagsand trueif the given HTTP request is authorized to access the provisioner daemon
// protobuf API, and returns nil,false otherwise.
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (map[string]string,bool) {
// authorize returns mutated tags if the given HTTP request is authorized to access the provisioner daemon
// protobuf API, and returns nil,err otherwise.
func (p *provisionerDaemonAuth) authorize(r *http.Request, orgID uuid.UUID, tags map[string]string) (map[string]string,error) {
ctx := r.Context()
apiKey, ok := httpmw.APIKeyOptional(r)
if ok {
apiKey, apiKeyOK := httpmw.APIKeyOptional(r)
pk, pkOK := httpmw.ProvisionerKeyAuthOptional(r)
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
if !provAuth && !apiKeyOK {
return nil, xerrors.New("no API key or provisioner key provided")
}
if apiKeyOK && pkOK {
return nil, xerrors.New("Both API key and provisioner key authentication provided. Only one is allowed.")
}

if apiKeyOK {
tags = provisionersdk.MutateTags(apiKey.UserID, tags)
if tags[provisionersdk.TagScope] == provisionersdk.ScopeUser {
// Any authenticated user can create provisioner daemons scoped
// for jobs that they own,
return tags,true
return tags,nil
}
ua := httpmw.UserAuthorization(r)
if err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID)); err == nil {
// User is allowed to create provisioner daemons
return tags, true
err := p.authorizer.Authorize(ctx, ua, policy.ActionCreate, rbac.ResourceProvisionerDaemon.InOrg(orgID))
if err != nil {
if !provAuth {
return nil, xerrors.New("user unauthorized")
}

// Allow fallback to PSK auth if the user is not allowed to create provisioner daemons.
// This is to preserve backwards compatibility with existing user provisioner daemons.
// If using PSK auth, the daemon is, by definition, scoped to the organization.
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return tags, nil
}

// User is allowed to create provisioner daemons
return tags, nil
}

// Check for PSK
provAuth := httpmw.ProvisionerDaemonAuthenticated(r)
if provAuth {
// If using PSK auth, the daemon is, by definition, scoped to the organization.
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return tags, true
if pkOK {
if pk.OrganizationID != orgID {
return nil, xerrors.New("provisioner key unauthorized")
}
}
return nil, false

// If using provisioner key / PSK auth, the daemon is, by definition, scoped to the organization.
tags = provisionersdk.MutateTags(uuid.Nil, tags)
return tags, nil
}

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

tags,authorized := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
if!authorized {
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags))
tags,err := api.provisionerDaemonAuth.authorize(r, organization.ID, tags)
iferr != nil {
api.Logger.Warn(ctx, "unauthorized provisioner daemon serve request", slog.F("tags", tags), slog.Error(err))
httpapi.Write(ctx, rw, http.StatusForbidden,
codersdk.Response{
Message: fmt.Sprintf("You aren't allowed to create provisioner daemons with scope %q", tags[provisionersdk.TagScope]),
Detail: err.Error(),
},
)
return
Expand DownExpand Up@@ -209,7 +232,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
)

authCtx := ctx
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != "" {
if r.Header.Get(codersdk.ProvisionerDaemonPSK) != ""|| r.Header.Get(codersdk.ProvisionerDaemonKey) != ""{
//nolint:gocritic // PSK auth means no actor in request,
// so use system restricted.
authCtx = dbauthz.AsSystemRestricted(ctx)
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp