From 64b7a80dcd8b311217668563242446580581bb59 Mon Sep 17 00:00:00 2001 From: Simon Emms Date: Fri, 10 Dec 2021 16:07:46 +0000 Subject: [PATCH 1/3] [installer]: configure the authProviders as an array of secrets --- installer/README.md | 40 +++++++++++----- installer/pkg/components/server/configmap.go | 12 ++++- installer/pkg/components/server/constants.go | 15 +++--- installer/pkg/components/server/deployment.go | 21 ++++++++ installer/pkg/components/server/types.go | 24 +++++----- installer/pkg/config/v1/config.go | 48 +++++++------------ installer/pkg/config/v1/validation.go | 43 +++++++++++++++++ 7 files changed, 139 insertions(+), 64 deletions(-) diff --git a/installer/README.md b/installer/README.md index 6732d878b1a704..87559aed0e9349 100644 --- a/installer/README.md +++ b/installer/README.md @@ -148,21 +148,39 @@ is `true`. External dependencies can be used in their place ## Auth Providers -> This may move to a secret in future [#6867](https://github.com/gitpod-io/gitpod/issues/6867) - Gitpod must be connected to a Git provider. This can be done via the -dashboard on first load, or by providing `authProviders` configuration. +dashboard on first load, or by providing `authProviders` configuration +as a Kubernetes secret. + +### Setting via config + +1. Update your configuration file: ```yaml authProviders: - - id: Public-GitHub - host: github.com - type: GitHub - oauth: - clientId: xxx - clientSecret: xxx - callBackUrl: https://$DOMAIN/auth/github.com/callback - settingsUrl: xxx + - kind: secret + name: public-github +``` + +2. Create a secret file: + +```yaml +# Save this public-github.yaml + +id: Public-GitHub +host: github.com +type: GitHub +oauth: + clientId: xxx + clientSecret: xxx + callBackUrl: https://$DOMAIN/auth/github.com/callback + settingsUrl: xxx +``` + +3. Create the secret: + +```shell +kubectl create secret generic --from-file=provider=./public-github.yaml public-github ``` ## In-cluster vs External Dependencies diff --git a/installer/pkg/components/server/configmap.go b/installer/pkg/components/server/configmap.go index 83918ff5ea3994..7d8ae23cdc548d 100644 --- a/installer/pkg/components/server/configmap.go +++ b/installer/pkg/components/server/configmap.go @@ -55,8 +55,16 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { MinAgeDays: 14, MinAgePrebuildDays: 7, }, - EnableLocalApp: true, - AuthProviderConfigs: ctx.Config.AuthProviders, + EnableLocalApp: true, + AuthProviderConfigFiles: func() []string { + providers := make([]string, 0) + + for _, provider := range ctx.Config.AuthProviders { + providers = append(providers, fmt.Sprintf("%s/%s", authProviderFilePath, provider.Name)) + } + + return providers + }(), BuiltinAuthProvidersConfigured: len(ctx.Config.AuthProviders) > 0, DisableDynamicAuthProviderLogin: false, BrandingConfig: BrandingConfig{ diff --git a/installer/pkg/components/server/constants.go b/installer/pkg/components/server/constants.go index e012789959b782..2cce882ba1b235 100644 --- a/installer/pkg/components/server/constants.go +++ b/installer/pkg/components/server/constants.go @@ -9,11 +9,12 @@ import ( ) const ( - Component = common.ServerComponent - ContainerPort = 3000 - ContainerPortName = "http" - licenseFilePath = "/gitpod/license" - PrometheusPort = 9500 - PrometheusPortName = "metrics" - ServicePort = 3000 + Component = common.ServerComponent + ContainerPort = 3000 + ContainerPortName = "http" + authProviderFilePath = "/gitpod/auth-providers" + licenseFilePath = "/gitpod/license" + PrometheusPort = 9500 + PrometheusPortName = "metrics" + ServicePort = 3000 ) diff --git a/installer/pkg/components/server/deployment.go b/installer/pkg/components/server/deployment.go index aad091f5d35e76..6e5ef2ce5e9c99 100644 --- a/installer/pkg/components/server/deployment.go +++ b/installer/pkg/components/server/deployment.go @@ -57,6 +57,27 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) { }) } + if len(ctx.Config.AuthProviders) > 0 { + for i, provider := range ctx.Config.AuthProviders { + volumeName := fmt.Sprintf("auth-provider-%d", i) + volumes = append(volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: provider.Name, + }, + }, + }) + + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: fmt.Sprintf("%s/%s", authProviderFilePath, provider.Name), + SubPath: "provider", + ReadOnly: true, + }) + } + } + return []runtime.Object{ &appsv1.Deployment{ TypeMeta: common.TypeMetaDeployment, diff --git a/installer/pkg/components/server/types.go b/installer/pkg/components/server/types.go index 7c3cdf4f62229b..d044c5febf4567 100644 --- a/installer/pkg/components/server/types.go +++ b/installer/pkg/components/server/types.go @@ -34,18 +34,18 @@ type ConfigSerialized struct { ChargebeeProviderOptionsFile string `json:"chargebeeProviderOptionsFile"` EnablePayment bool `json:"enablePayment"` - WorkspaceHeartbeat WorkspaceHeartbeat `json:"workspaceHeartbeat"` - WorkspaceDefaults WorkspaceDefaults `json:"workspaceDefaults"` - Session Session `json:"session"` - GitHubApp GitHubApp `json:"githubApp"` - WorkspaceGarbageCollection WorkspaceGarbageCollection `json:"workspaceGarbageCollection"` - AuthProviderConfigs []config.AuthProviderConfigs `json:"authProviderConfigs"` - BrandingConfig BrandingConfig `json:"brandingConfig"` - IncrementalPrebuilds IncrementalPrebuilds `json:"incrementalPrebuilds"` - BlockNewUsers config.BlockNewUsers `json:"blockNewUsers"` - OAuthServer OAuthServer `json:"oauthServer"` - RateLimiter RateLimiter `json:"rateLimiter"` - CodeSync CodeSync `json:"codeSync"` + WorkspaceHeartbeat WorkspaceHeartbeat `json:"workspaceHeartbeat"` + WorkspaceDefaults WorkspaceDefaults `json:"workspaceDefaults"` + Session Session `json:"session"` + GitHubApp GitHubApp `json:"githubApp"` + WorkspaceGarbageCollection WorkspaceGarbageCollection `json:"workspaceGarbageCollection"` + AuthProviderConfigFiles []string `json:"authProviderConfigFiles"` + BrandingConfig BrandingConfig `json:"brandingConfig"` + IncrementalPrebuilds IncrementalPrebuilds `json:"incrementalPrebuilds"` + BlockNewUsers config.BlockNewUsers `json:"blockNewUsers"` + OAuthServer OAuthServer `json:"oauthServer"` + RateLimiter RateLimiter `json:"rateLimiter"` + CodeSync CodeSync `json:"codeSync"` } type CodeSyncResources struct { diff --git a/installer/pkg/config/v1/config.go b/installer/pkg/config/v1/config.go index c3434a120f42b7..575b0696e6dd02 100644 --- a/installer/pkg/config/v1/config.go +++ b/installer/pkg/config/v1/config.go @@ -21,7 +21,7 @@ type version struct{} func (v version) Factory() interface{} { return &Config{ - AuthProviders: []AuthProviderConfigs{}, + AuthProviders: []ObjectRef{}, BlockNewUsers: BlockNewUsers{ Enabled: false, Passlist: []string{}, @@ -81,9 +81,9 @@ type Config struct { Workspace Workspace `json:"workspace" validate:"required"` - AuthProviders []AuthProviderConfigs `json:"authProviders" validate:"dive"` - BlockNewUsers BlockNewUsers `json:"blockNewUsers"` - License *ObjectRef `json:"license,omitempty"` + AuthProviders []ObjectRef `json:"authProviders" validate:"dive"` + BlockNewUsers BlockNewUsers `json:"blockNewUsers"` + License *ObjectRef `json:"license,omitempty"` } type Metadata struct { @@ -229,38 +229,22 @@ const ( FSShiftShiftFS FSShiftMethod = "shiftfs" ) -// todo(sje): I don't know if we want to put this in the config YAML -type AuthProviderConfigs struct { - ID string `json:"id" validate:"required"` - Host string `json:"host" validate:"required"` - Type string `json:"type" validate:"required"` - BuiltIn string `json:"builtin"` - Verified string `json:"verified"` - OAuth OAuth `json:"oauth" validate:"required"` - Params map[string]string `json:"params"` - HiddenOnDashboard bool `json:"hiddenOnDashboard"` - LoginContextMatcher string `json:"loginContextMatcher"` - DisallowLogin bool `json:"disallowLogin"` - RequireTOS bool `json:"requireTOS"` - Description string `json:"description"` - Icon string `json:"icon"` -} - type BlockNewUsers struct { Enabled bool `json:"enabled"` Passlist []string `json:"passlist"` } +// AuthProviderConfigs this only contains what is necessary for validation +type AuthProviderConfigs struct { + ID string `json:"id" validate:"required"` + Host string `json:"host" validate:"required"` + Type string `json:"type" validate:"required"` + OAuth OAuth `json:"oauth" validate:"required"` +} + +// OAuth this only contains what is necessary for validation type OAuth struct { - ClientId string `json:"clientId" validate:"required"` - ClientSecret string `json:"clientSecret" validate:"required"` - CallBackUrl string `json:"callBackUrl" validate:"required"` - AuthorizationUrl string `json:"authorizationUrl"` - TokenUrl string `json:"tokenUrl"` - Scope string `json:"scope"` - ScopeSeparator string `json:"scopeSeparator"` - SettingsUrl string `json:"settingsUrl"` - AuthorizationParams map[string]string `json:"authorizationParams"` - ConfigURL string `json:"configURL"` - ConfigFn string `json:"configFn"` + ClientId string `json:"clientId" validate:"required"` + ClientSecret string `json:"clientSecret" validate:"required"` + CallBackUrl string `json:"callBackUrl" validate:"required"` } diff --git a/installer/pkg/config/v1/validation.go b/installer/pkg/config/v1/validation.go index a02a2be2a282b6..4012f9fea683e6 100644 --- a/installer/pkg/config/v1/validation.go +++ b/installer/pkg/config/v1/validation.go @@ -5,9 +5,13 @@ package config import ( + "fmt" + "github.com/gitpod-io/gitpod/installer/pkg/cluster" + "sigs.k8s.io/yaml" "github.com/go-playground/validator/v10" + corev1 "k8s.io/api/core/v1" ) var InstallationKindList = map[InstallationKind]struct{}{ @@ -107,5 +111,44 @@ func (v version) ClusterValidation(rcfg interface{}) cluster.ValidationChecks { res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("license"))) } + if len(cfg.AuthProviders) > 0 { + for _, provider := range cfg.AuthProviders { + secretName := provider.Name + secretKey := "provider" + res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData(secretKey), cluster.CheckSecretRule(func(s *corev1.Secret) ([]cluster.ValidationError, error) { + errors := make([]cluster.ValidationError, 0) + providerData := s.Data[secretKey] + + var provider AuthProviderConfigs + err := yaml.Unmarshal(providerData, &provider) + if err != nil { + return nil, err + } + + validate := validator.New() + err = v.LoadValidationFuncs(validate) + if err != nil { + return nil, err + } + + err = validate.Struct(provider) + if err != nil { + validationErrors := err.(validator.ValidationErrors) + + if len(validationErrors) > 0 { + for _, v := range validationErrors { + errors = append(errors, cluster.ValidationError{ + Message: fmt.Sprintf("Field '%s' failed %s validation", v.Namespace(), v.Tag()), + Type: cluster.ValidationStatusError, + }) + } + } + } + + return errors, nil + }))) + } + } + return res } From ba0eca0f813cd1ae85d200a3d5273c3964474cb5 Mon Sep 17 00:00:00 2001 From: Simon Emms Date: Fri, 10 Dec 2021 16:38:38 +0000 Subject: [PATCH 2/3] [server]: allow auth provider config to be passed in via file path This allows for the values to be injected via a Kubernetes secret, reducing the exposure of sensitive data in configuration --- components/server/src/config.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/components/server/src/config.ts b/components/server/src/config.ts index c0d06080df963f..ce350ae794528a 100644 --- a/components/server/src/config.ts +++ b/components/server/src/config.ts @@ -13,6 +13,7 @@ import { RateLimiterConfig } from './auth/rate-limiter'; import { CodeSyncConfig } from './code-sync/code-sync-service'; import { ChargebeeProviderOptions, readOptionsFromFile } from "@gitpod/gitpod-payment-endpoint/lib/chargebee"; import * as fs from 'fs'; +import * as yaml from 'js-yaml'; import { log, LogrusLogLevel } from '@gitpod/gitpod-protocol/lib/util/logging'; import { filePathTelepresenceAware, KubeStage, translateLegacyStagename } from '@gitpod/gitpod-protocol/lib/env'; import { BrandingParser } from './branding-parser'; @@ -86,6 +87,7 @@ export interface ConfigSerialized { enableLocalApp: boolean; authProviderConfigs: AuthProviderParams[]; + authProviderConfigFiles: string[]; builtinAuthProvidersConfigured: boolean; disableDynamicAuthProviderLogin: boolean; @@ -170,10 +172,25 @@ export namespace ConfigFile { function loadAndCompleteConfig(config: ConfigSerialized): Config { const hostUrl = new GitpodHostUrl(config.hostUrl); - let authProviderConfigs = config.authProviderConfigs - if (authProviderConfigs) { - authProviderConfigs = normalizeAuthProviderParams(authProviderConfigs); + let authProviderConfigs: AuthProviderParams[] = [] + const rawProviderConfigs = config.authProviderConfigs + if (rawProviderConfigs) { + /* Add raw provider data */ + authProviderConfigs.push(...rawProviderConfigs); } + const rawProviderConfigFiles = config.authProviderConfigFiles + if (rawProviderConfigFiles) { + /* Add providers from files */ + const authProviderConfigFiles: AuthProviderParams[] = rawProviderConfigFiles.map((providerFile) => { + const rawProviderData = fs.readFileSync(providerFile, "utf-8") + + return yaml.load(rawProviderData) as AuthProviderParams + }); + + authProviderConfigs.push(...authProviderConfigFiles); + } + authProviderConfigs = normalizeAuthProviderParams(authProviderConfigs) + const builtinAuthProvidersConfigured = authProviderConfigs.length > 0; const chargebeeProviderOptions = readOptionsFromFile(filePathTelepresenceAware(config.chargebeeProviderOptionsFile || "")); let brandingConfig = config.brandingConfig; @@ -183,7 +200,7 @@ export namespace ConfigFile { let license = config.license const licenseFile = config.licenseFile if (licenseFile) { - license = fs.readFileSync(licenseFile, "utf-8") + license = fs.readFileSync(licenseFile, "utf-8"); } return { ...config, From 9d41659823d4bcf0383bf099ddb5077017089e8d Mon Sep 17 00:00:00 2001 From: Simon Emms Date: Tue, 14 Dec 2021 16:46:10 +0000 Subject: [PATCH 3/3] [werft] pull provider secret into build and configure installer --- .werft/build.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.werft/build.ts b/.werft/build.ts index ce1b6286410c18..239b9f6f4c17f6 100644 --- a/.werft/build.ts +++ b/.werft/build.ts @@ -477,14 +477,26 @@ export async function deployToDevWithInstaller(deploymentConfig: DeploymentConfi exec(`yq w -i config.yaml observability.tracing.endpoint ${tracingEndpoint}`, {slice: installerSlices.INSTALLER_RENDER}); } - // TODO: Remove this after #6867 is done - werft.log("authProviders", "copy authProviders") + werft.log("authProviders", "copy authProviders from secret") try { - exec(`kubectl get secret preview-envs-authproviders --namespace=keys -o yaml \ - | yq r - data.authProviders \ + exec(`for row in $(kubectl get secret preview-envs-authproviders --namespace=keys -o jsonpath="{.data.authProviders}" \ | base64 -d -w 0 \ - > ./authProviders`, { silent: true }); - exec(`yq merge --inplace config.yaml ./authProviders`, { silent: true }) + | yq r - authProviders -j \ + | jq -r 'to_entries | .[] | @base64'); do + key=$(echo $row | base64 -d | jq -r '.key') + providerId=$(echo $row | base64 -d | jq -r '.value.id | ascii_downcase') + data=$(echo $row | base64 -d | yq r - value --prettyPrint) + + yq w -i ./config.yaml authProviders[$key].kind "secret" + yq w -i ./config.yaml authProviders[$key].name "$providerId" + + kubectl create secret generic "$providerId" \ + --namespace "${namespace}" \ + --from-literal=provider="$data" \ + --dry-run=client -o yaml | \ + kubectl replace --force -f - + done`, { silent: true }) + werft.done('authProviders'); } catch (err) { werft.fail('authProviders', err);