git.iko.soy/typeable/oauth2-proxy
on master help sign in
Workload identity support (#2126)

* WIP: support for workload identity * WIP: bugfixes to support WI * Added support for Workload Identity * Added missing flag * Refactoring and typo * Updated CHANGELOG.md * Updated docs * Updated changelog * Improved readability and fixed codeclimate issues * Update CHANGELOG.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Fixed if statement * Apply suggestions from code review Co-authored-by: Jan Larwig <jan@larwig.com> * Cleanup * Removed target principal * Removed references to target principal * Added docs * Fixed header anchor linking * Update auth.md * Updated generated code * Improved code * Fixed tests --------- Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> Co-authored-by: Jan Larwig <jan@larwig.com>

commita6e8ec81e8a70208ac9282b1cc70361ee9c0ab64 authorKoen van Zuijlen <8818390+kvanzuijlen@users.noreply.github.com> dateMon 4 Sep 2023, 11:34 (3 years ago) parent738c090
9 files changed+114−60
CHANGELOG.md+1 docs/docs/configuration/alpha_config.md+1 docs/docs/configuration/auth.md+11−1 docs/docs/configuration/overview.md+1 pkg/apis/options/legacy_options.go+32−21 pkg/apis/options/providers.go+2 pkg/validation/options_test.go+2−2 pkg/validation/providers.go+25−13 providers/google.go+39−23
CHANGELOG.md+1
@@ -22,6 +22,7 @@
2222- [#1988](https://github.com/oauth2-proxy/oauth2-proxy/pull/1988) Ensure sign-in page background is uniform throughout the page
2323- [#2013](https://github.com/oauth2-proxy/oauth2-proxy/pull/2013) Upgrade alpine to version 3.17.2 and library dependencies (@miguelborges99)
2424- [#2047](https://github.com/oauth2-proxy/oauth2-proxy/pull/2047) CVE-2022-41717: DoS in Go net/http may lead to DoS (@miguelborges99)
25+- [#2126](https://github.com/oauth2-proxy/oauth2-proxy/pull/2126) Added support for GKE Workload Identity (@kvanzuijlen)
2526- [#1921](https://github.com/oauth2-proxy/oauth2-proxy/pull/1921) Check jsonpath syntax before interpretation
2627- [#2025](https://github.com/oauth2-proxy/oauth2-proxy/pull/2025) Embed static stylesheets and dependencies
2728
docs/docs/configuration/alpha_config.md+1
@@ -236,6 +236,7 @@ Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
236236| `group` | _[]string_ | Groups sets restrict logins to members of this google group |
237237| `adminEmail` | _string_ | AdminEmail is the google admin to impersonate for api calls |
238238| `serviceAccountJson` | _string_ | ServiceAccountJSON is the path to the service account json credentials |
239+| `useApplicationDefaultCredentials` | _bool_ | UseApplicationDefaultCredentials is a boolean whether to use Application Default Credentials instead of a ServiceAccountJSON |
239240
240241### Header
241242
docs/docs/configuration/auth.md+11−1
@@ -50,12 +50,22 @@ It's recommended to refresh sessions on a short interval (1h) with `cookie-refre
5050
5151#### Restrict auth to specific Google groups on your domain. (optional)
5252
53−1. Create a service account: https://developers.google.com/identity/protocols/OAuth2ServiceAccount and make sure to download the json file.
53+1. Create a [service account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount) and download the json
54+file if you're not using [Application Default Credentials / Workload Identity / Workload Identity Federation (recommended)](#using-application-default-credentials-adc--workload-identity--workload-identity-federation-recommended).
54552. Make note of the Client ID for a future step.
55563. Under "APIs & Auth", choose APIs.
56574. Click on Admin SDK and then Enable API.
57585. Follow the steps on https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account and give the client id from step 2 the following oauth scopes:
5859
60+
61+##### Using Application Default Credentials (ADC) / Workload Identity / Workload Identity Federation (recommended)
62+oauth2-proxy can make use of [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials).
63+When deployed within GCP, this means that it can automatically use the service account attached to the resource. When deployed to GKE, ADC
64+can be leveraged through a feature called Workload Identity. Follow Google's [guide](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)
65+to set up Workload Identity.
66+
67+When deployed outside of GCP, [Workload Identity Federation](https://cloud.google.com/docs/authentication/provide-credentials-adc#wlif) might be an option.
68+
5969```
6070https://www.googleapis.com/auth/admin.directory.group.readonly
6171https://www.googleapis.com/auth/admin.directory.user.readonly
docs/docs/configuration/overview.md+1
@@ -120,6 +120,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
120120| `--google-admin-email` | string | the google admin to impersonate for api calls | |
121121| `--google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
122122| `--google-service-account-json` | string | the path to the service account json credentials | |
123+| `--google-use-application-default-credentials` | bool | use application default credentials instead of service account json (i.e. GKE Workload Identity) | |
123124| `--htpasswd-file` | string | additionally authenticate against a htpasswd file. Entries must be created with `htpasswd -B` for bcrypt encryption | |
124125| `--htpasswd-user-group` | string \| list | the groups to be set on sessions for htpasswd users | |
125126| `--http-address` | string | `[http://]<addr>:<port>` or `unix://<path>` to listen on for HTTP clients. Square brackets are required for ipv6 address, e.g. `http://[::1]:4180` | `"127.0.0.1:4180"` |
pkg/apis/options/legacy_options.go+32−21
@@ -70,6 +70,7 @@ func NewLegacyFlagSet() *pflag.FlagSet {
7070 flagSet.AddFlagSet(legacyHeadersFlagSet())
7171 flagSet.AddFlagSet(legacyServerFlagset())
7272 flagSet.AddFlagSet(legacyProviderFlagSet())
73+ flagSet.AddFlagSet(legacyGoogleFlagSet())
7374
7475 return flagSet
7576}
@@ -481,21 +482,22 @@ type LegacyProvider struct {
481482 ClientSecret string `flag:"client-secret" cfg:"client_secret"`
482483 ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"`
483484
484− KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
485− AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
486− AzureGraphGroupField string `flag:"azure-graph-group-field" cfg:"azure_graph_group_field"`
487− BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
488− BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
489− GitHubOrg string `flag:"github-org" cfg:"github_org"`
490− GitHubTeam string `flag:"github-team" cfg:"github_team"`
491− GitHubRepo string `flag:"github-repo" cfg:"github_repo"`
492− GitHubToken string `flag:"github-token" cfg:"github_token"`
493− GitHubUsers []string `flag:"github-user" cfg:"github_users"`
494− GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"`
495− GitLabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"`
496− GoogleGroups []string `flag:"google-group" cfg:"google_group"`
497− GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
498− GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
485+ KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
486+ AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
487+ AzureGraphGroupField string `flag:"azure-graph-group-field" cfg:"azure_graph_group_field"`
488+ BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
489+ BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
490+ GitHubOrg string `flag:"github-org" cfg:"github_org"`
491+ GitHubTeam string `flag:"github-team" cfg:"github_team"`
492+ GitHubRepo string `flag:"github-repo" cfg:"github_repo"`
493+ GitHubToken string `flag:"github-token" cfg:"github_token"`
494+ GitHubUsers []string `flag:"github-user" cfg:"github_users"`
495+ GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"`
496+ GitLabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"`
497+ GoogleGroups []string `flag:"google-group" cfg:"google_group"`
498+ GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
499+ GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
500+ GoogleUseApplicationDefaultCredentials bool `flag:"google-use-application-default-credentials" cfg:"google_use_application_default_credentials"`
499501
500502 // These options allow for other providers besides Google, with
501503 // potential overrides.
@@ -549,9 +551,6 @@ func legacyProviderFlagSet() *pflag.FlagSet {
549551 flagSet.StringSlice("github-user", []string{}, "allow users with these usernames to login even if they do not belong to the specified org and team or collaborators (may be given multiple times)")
550552 flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
551553 flagSet.StringSlice("gitlab-project", []string{}, "restrict logins to members of this project (may be given multiple times) (eg `group/project=accesslevel`). Access level should be a value matching Gitlab access levels (see https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent")
552− flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
553− flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
554− flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
555554 flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"")
556555 flagSet.String("client-secret", "", "the OAuth Client Secret")
557556 flagSet.String("client-secret-file", "", "the file with OAuth Client Secret")
@@ -592,6 +591,17 @@ func legacyProviderFlagSet() *pflag.FlagSet {
592591 return flagSet
593592}
594593
594+func legacyGoogleFlagSet() *pflag.FlagSet {
595+ flagSet := pflag.NewFlagSet("google", pflag.ExitOnError)
596+
597+ flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
598+ flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
599+ flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
600+ flagSet.String("google-use-application-default-credentials", "", "use application default credentials instead of service account json (i.e. GKE Workload Identity)")
601+
602+ return flagSet
603+}
604+
595605func (l LegacyServer) convert() (Server, Server) {
596606 appServer := Server{
597607 BindAddress: l.HTTPAddress,
@@ -718,9 +728,10 @@ func (l *LegacyProvider) convert() (Providers, error) {
718728 }
719729 case "google":
720730 provider.GoogleConfig = GoogleOptions{
721− Groups: l.GoogleGroups,
722− AdminEmail: l.GoogleAdminEmail,
723− ServiceAccountJSON: l.GoogleServiceAccountJSON,
731+ Groups: l.GoogleGroups,
732+ AdminEmail: l.GoogleAdminEmail,
733+ ServiceAccountJSON: l.GoogleServiceAccountJSON,
734+ UseApplicationDefaultCredentials: l.GoogleUseApplicationDefaultCredentials,
724735 }
725736 }
726737
pkg/apis/options/providers.go+2
@@ -189,6 +189,8 @@ type GoogleOptions struct {
189189 AdminEmail string `json:"adminEmail,omitempty"`
190190 // ServiceAccountJSON is the path to the service account json credentials
191191 ServiceAccountJSON string `json:"serviceAccountJson,omitempty"`
192+ // UseApplicationDefaultCredentials is a boolean whether to use Application Default Credentials instead of a ServiceAccountJSON
193+ UseApplicationDefaultCredentials bool `json:"useApplicationDefaultCredentials,omitempty"`
192194}
193195
194196type OIDCOptions struct {
pkg/validation/options_test.go+2−2
@@ -63,7 +63,7 @@ func TestGoogleGroupOptions(t *testing.T) {
6363
6464 expected := errorMsg([]string{
6565 "missing setting: google-admin-email",
66− "missing setting: google-service-account-json"})
66+ "missing setting: google-service-account-json or google-use-application-default-credentials"})
6767 assert.Equal(t, expected, err.Error())
6868}
6969
@@ -76,7 +76,7 @@ func TestGoogleGroupInvalidFile(t *testing.T) {
7676 assert.NotEqual(t, nil, err)
7777
7878 expected := errorMsg([]string{
79− "invalid Google credentials file: file_doesnt_exist.json",
79+ "Google credentials file not found: file_doesnt_exist.json",
8080 })
8181 assert.Equal(t, expected, err.Error())
8282}
pkg/validation/providers.go+25−13
@@ -66,20 +66,32 @@ func validateProvider(provider options.Provider, providerIDs map[string]struct{}
6666
6767func validateGoogleConfig(provider options.Provider) []string {
6868 msgs := []string{}
69− if len(provider.GoogleConfig.Groups) > 0 ||
70− provider.GoogleConfig.AdminEmail != "" ||
71− provider.GoogleConfig.ServiceAccountJSON != "" {
72− if len(provider.GoogleConfig.Groups) < 1 {
73− msgs = append(msgs, "missing setting: google-group")
74− }
75− if provider.GoogleConfig.AdminEmail == "" {
76− msgs = append(msgs, "missing setting: google-admin-email")
77− }
78− if provider.GoogleConfig.ServiceAccountJSON == "" {
79− msgs = append(msgs, "missing setting: google-service-account-json")
80− } else if _, err := os.Stat(provider.GoogleConfig.ServiceAccountJSON); err != nil {
81− msgs = append(msgs, fmt.Sprintf("invalid Google credentials file: %s", provider.GoogleConfig.ServiceAccountJSON))
69+
70+ hasGoogleGroups := len(provider.GoogleConfig.Groups) >= 1
71+ hasAdminEmail := provider.GoogleConfig.AdminEmail != ""
72+ hasSAJSON := provider.GoogleConfig.ServiceAccountJSON != ""
73+ useADC := provider.GoogleConfig.UseApplicationDefaultCredentials
74+
75+ if !hasGoogleGroups && !hasAdminEmail && !hasSAJSON && !useADC {
76+ return msgs
77+ }
78+
79+ if !hasGoogleGroups {
80+ msgs = append(msgs, "missing setting: google-group")
81+ }
82+ if !hasAdminEmail {
83+ msgs = append(msgs, "missing setting: google-admin-email")
84+ }
85+
86+ _, err := os.Stat(provider.GoogleConfig.ServiceAccountJSON)
87+ if !useADC {
88+ if !hasSAJSON {
89+ msgs = append(msgs, "missing setting: google-service-account-json or google-use-application-default-credentials")
90+ } else if err != nil {
91+ msgs = append(msgs, fmt.Sprintf("Google credentials file not found: %s", provider.GoogleConfig.ServiceAccountJSON))
8292 }
93+ } else if hasSAJSON {
94+ msgs = append(msgs, "invalid setting: can't use both google-service-account-json and google-use-application-default-credentials")
8395 }
8496
8597 return msgs
providers/google.go+39−23
@@ -8,6 +8,7 @@ import (
88 "errors"
99 "fmt"
1010 "io"
11+ "net/http"
1112 "net/url"
1213 "os"
1314 "strings"
@@ -17,6 +18,7 @@ import (
1718 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1819 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1920 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
21+ "golang.org/x/oauth2"
2022 "golang.org/x/oauth2/google"
2123 admin "google.golang.org/api/admin/directory/v1"
2224 "google.golang.org/api/googleapi"
@@ -98,17 +100,13 @@ func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProv
98100 },
99101 }
100102
101− if opts.ServiceAccountJSON != "" {
102− file, err := os.Open(opts.ServiceAccountJSON)
103− if err != nil {
104− return nil, fmt.Errorf("invalid Google credentials file: %s", opts.ServiceAccountJSON)
105− }
106−
103+ if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials {
107104 // Backwards compatibility with `--google-group` option
108105 if len(opts.Groups) > 0 {
109106 provider.setAllowedGroups(opts.Groups)
110107 }
111− provider.setGroupRestriction(opts.Groups, opts.AdminEmail, file)
108+
109+ provider.setGroupRestriction(opts)
112110 }
113111
114112 return provider, nil
@@ -214,13 +212,13 @@ func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionSta
214212// account credentials.
215213//
216214// TODO (@NickMeves) - Unit Test this OR refactor away from groupValidator func
217−func (p *GoogleProvider) setGroupRestriction(groups []string, adminEmail string, credentialsReader io.Reader) {
218− adminService := getAdminService(adminEmail, credentialsReader)
215+func (p *GoogleProvider) setGroupRestriction(opts options.GoogleOptions) {
216+ adminService := getAdminService(opts)
219217 p.groupValidator = func(s *sessions.SessionState) bool {
220218 // Reset our saved Groups in case membership changed
221219 // This is used by `Authorize` on every request
222− s.Groups = make([]string, 0, len(groups))
223− for _, group := range groups {
220+ s.Groups = make([]string, 0, len(opts.Groups))
221+ for _, group := range opts.Groups {
224222 if userInGroup(adminService, group, s.Email) {
225223 s.Groups = append(s.Groups, group)
226224 }
@@ -229,19 +227,37 @@ func (p *GoogleProvider) setGroupRestriction(groups []string, adminEmail string,
229227 }
230228}
231229
232−func getAdminService(adminEmail string, credentialsReader io.Reader) *admin.Service {
233− data, err := io.ReadAll(credentialsReader)
234− if err != nil {
235− logger.Fatal("can't read Google credentials file:", err)
236− }
237− conf, err := google.JWTConfigFromJSON(data, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope)
238− if err != nil {
239− logger.Fatal("can't load Google credentials file:", err)
240− }
241− conf.Subject = adminEmail
242−
230+func getAdminService(opts options.GoogleOptions) *admin.Service {
243231 ctx := context.Background()
244− client := conf.Client(ctx)
232+ var client *http.Client
233+ if opts.UseApplicationDefaultCredentials {
234+ ts, err := google.FindDefaultCredentialsWithParams(ctx, google.CredentialsParams{
235+ Subject: opts.AdminEmail,
236+ Scopes: []string{admin.AdminDirectoryGroupReadonlyScope, admin.AdminDirectoryUserReadonlyScope},
237+ })
238+ if err != nil {
239+ logger.Fatal("failed to fetch application default credentials: ", err)
240+ }
241+ client = oauth2.NewClient(ctx, ts.TokenSource)
242+ } else {
243+ credentialsReader, err := os.Open(opts.ServiceAccountJSON)
244+ if err != nil {
245+ logger.Fatal("couldn't open Google credentials file: ", err)
246+ return nil
247+ }
248+
249+ data, err := io.ReadAll(credentialsReader)
250+ if err != nil {
251+ logger.Fatal("can't read Google credentials file:", err)
252+ }
253+
254+ conf, err := google.JWTConfigFromJSON(data, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope)
255+ if err != nil {
256+ logger.Fatal("can't load Google credentials file:", err)
257+ }
258+ conf.Subject = opts.AdminEmail
259+ client = conf.Client(ctx)
260+ }
245261 adminService, err := admin.NewService(ctx, option.WithHTTPClient(client))
246262 if err != nil {
247263 logger.Fatal(err)