Skip to content

feat: add PSK for external provisionerd auth #8877

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 4, 2023
Merged
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
18 changes: 14 additions & 4 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,15 @@ func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
// InitClient sets client to a new client.
// It reads from global configuration files if flags are not set.
func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
return r.initClientInternal(client, false)
}

func (r *RootCmd) InitClientMissingTokenOK(client *codersdk.Client) clibase.MiddlewareFunc {
return r.initClientInternal(client, true)
}

// nolint: revive
func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing bool) clibase.MiddlewareFunc {
if client == nil {
panic("client is nil")
}
Expand All @@ -508,7 +517,7 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
rawURL, err := conf.URL().Read()
// If the configuration files are absent, the user is logged out
if os.IsNotExist(err) {
return (errUnauthenticated)
return errUnauthenticated
}
if err != nil {
return err
Expand All @@ -524,9 +533,10 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
r.token, err = conf.Session().Read()
// If the configuration files are absent, the user is logged out
if os.IsNotExist(err) {
return (errUnauthenticated)
}
if err != nil {
if !allowTokenMissing {
return errUnauthenticated
}
} else if err != nil {
return err
}
}
Expand Down
4 changes: 4 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ updating, and deleting workspace resources.
--provisioner-daemon-poll-jitter duration, $CODER_PROVISIONER_DAEMON_POLL_JITTER (default: 100ms)
Random jitter added to the poll interval.

--provisioner-daemon-psk string, $CODER_PROVISIONER_DAEMON_PSK
Pre-shared key to authenticate external provisioner daemons to Coder
server.

--provisioner-daemons int, $CODER_PROVISIONER_DAEMONS (default: 3)
Number of provisioner daemons to create on start. If builds are stuck
in queued state for a long time, consider increasing this.
Expand Down
3 changes: 3 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ provisioning:
# Time to force cancel provisioning tasks that are stuck.
# (default: 10m0s, type: duration)
forceCancelInterval: 10m0s
# Pre-shared key to authenticate external provisioner daemons to Coder server.
# (default: <unset>, type: string)
daemonPSK: ""
# Enable one or more experiments. These are not ready for production. Separate
# multiple experiments with commas, or enter '*' to opt-in to all available
# experiments.
Expand Down
3 changes: 3 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,11 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui
}()

closer := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
return client.ServeProvisionerDaemon(ctx, org, []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, tags)
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
Organization: org,
Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho},
Tags: tags,
})
}, &provisionerd.Options{
Filesystem: fs,
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
Expand Down
3 changes: 3 additions & 0 deletions codersdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const (
// command that was invoked to produce the request. It is for internal use
// only.
CLITelemetryHeader = "Coder-CLI-Telemetry"

// ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon
ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK"
)

// loggableMimeTypes is a list of MIME types that are safe to log
Expand Down
10 changes: 10 additions & 0 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ type ProvisionerConfig struct {
DaemonPollInterval clibase.Duration `json:"daemon_poll_interval" typescript:",notnull"`
DaemonPollJitter clibase.Duration `json:"daemon_poll_jitter" typescript:",notnull"`
ForceCancelInterval clibase.Duration `json:"force_cancel_interval" typescript:",notnull"`
DaemonPSK clibase.String `json:"daemon_psk" typescript:",notnull"`
}

type RateLimitConfig struct {
Expand Down Expand Up @@ -1230,6 +1231,15 @@ when required by your organization's security policy.`,
Group: &deploymentGroupProvisioning,
YAML: "forceCancelInterval",
},
{
Name: "Provisioner Daemon Pre-shared Key (PSK)",
Description: "Pre-shared key to authenticate external provisioner daemons to Coder server.",
Flag: "provisioner-daemon-psk",
Env: "CODER_PROVISIONER_DAEMON_PSK",
Value: &c.Provisioner.DaemonPSK,
Group: &deploymentGroupProvisioning,
YAML: "daemonPSK",
},
// RateLimit settings
{
Name: "Disable All Rate Limits",
Expand Down
5 changes: 3 additions & 2 deletions codersdk/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,11 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization,
return organization, json.NewDecoder(res.Body).Decode(&organization)
}

// ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization.
// ProvisionerDaemons returns provisioner daemons available.
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
res, err := c.Request(ctx, http.MethodGet,
"/api/v2/provisionerdaemons",
// TODO: the organization path parameter is currently ignored.
"/api/v2/organizations/default/provisionerdaemons",
nil,
)
if err != nil {
Expand Down
51 changes: 37 additions & 14 deletions codersdk/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,38 +164,61 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
}), nil
}

// ListenProvisionerDaemon returns the gRPC service for a provisioner daemon
// ServeProvisionerDaemonRequest are the parameters to call ServeProvisionerDaemon with
// @typescript-ignore ServeProvisionerDaemonRequest
type ServeProvisionerDaemonRequest struct {
// Organization is the organization for the URL. At present provisioner daemons ARE NOT scoped to organizations
// and so the organization ID is optional.
Organization uuid.UUID `json:"organization" format:"uuid"`
// Provisioners is a list of provisioner types hosted by the provisioner daemon
Provisioners []ProvisionerType `json:"provisioners"`
// Tags is a map of key-value pairs that tag the jobs this provisioner daemon can handle
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"`
}

// ServeProvisionerDaemon returns the gRPC service for a provisioner daemon
// implementation. The context is during dial, not during the lifetime of the
// client. Client should be closed after use.
func (c *Client) ServeProvisionerDaemon(ctx context.Context, organization uuid.UUID, provisioners []ProvisionerType, tags map[string]string) (proto.DRPCProvisionerDaemonClient, error) {
serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons/serve", organization))
func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisionerDaemonRequest) (proto.DRPCProvisionerDaemonClient, error) {
serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons/serve", req.Organization))
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
}
query := serverURL.Query()
for _, provisioner := range provisioners {
for _, provisioner := range req.Provisioners {
query.Add("provisioner", string(provisioner))
}
for key, value := range tags {
for key, value := range req.Tags {
query.Add("tag", fmt.Sprintf("%s=%s", key, value))
}
serverURL.RawQuery = query.Encode()
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(serverURL, []*http.Cookie{{
Name: SessionTokenCookie,
Value: c.SessionToken(),
}})
httpClient := &http.Client{
Jar: jar,
Transport: c.HTTPClient.Transport,
}
headers := http.Header{}

if req.PreSharedKey == "" {
// use session token if we don't have a PSK.
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(serverURL, []*http.Cookie{{
Name: SessionTokenCookie,
Value: c.SessionToken(),
}})
httpClient.Jar = jar
} else {
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
}

conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
// Need to disable compression to avoid a data-race.
CompressionMode: websocket.CompressionDisabled,
HTTPHeader: headers,
})
if err != nil {
if res == nil {
Expand Down
1 change: 1 addition & 0 deletions docs/api/general.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions docs/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions docs/cli/provisionerd_start.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions docs/cli/server.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 15 additions & 9 deletions enterprise/cli/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
rawTags []string
pollInterval time.Duration
pollJitter time.Duration
preSharedKey string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: "start",
Short: "Run a provisioner daemon",
Middleware: clibase.Chain(
r.InitClient(client),
r.InitClientMissingTokenOK(client),
),
Handler: func(inv *clibase.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
Expand All @@ -59,11 +60,6 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
notifyCtx, notifyStop := signal.NotifyContext(ctx, agpl.InterruptSignals...)
defer notifyStop()

org, err := agpl.CurrentOrganization(inv, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}

tags, err := agpl.ParseProvisionerTags(rawTags)
if err != nil {
return err
Expand Down Expand Up @@ -112,9 +108,13 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(terraformClient),
}
srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
return client.ServeProvisionerDaemon(ctx, org.ID, []codersdk.ProvisionerType{
codersdk.ProvisionerTypeTerraform,
}, tags)
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
Provisioners: []codersdk.ProvisionerType{
codersdk.ProvisionerTypeTerraform,
},
Tags: tags,
PreSharedKey: preSharedKey,
})
}, &provisionerd.Options{
Logger: logger,
JobPollInterval: pollInterval,
Expand Down Expand Up @@ -182,6 +182,12 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
Default: (100 * time.Millisecond).String(),
Value: clibase.DurationOf(&pollJitter),
},
{
Flag: "psk",
Env: "CODER_PROVISIONER_DAEMON_PSK",
Description: "Pre-shared key to authenticate with Coder server.",
Value: clibase.StringOf(&preSharedKey),
},
}

return cmd
Expand Down
Loading