-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Gitpod OIDC Identity Provider #16482
Conversation
047204b
to
488b79d
Compare
b95fd99
to
923a5b4
Compare
/werft with-preview 👎 unknown command: with-preview |
/werft run with-preview 👍 started the job as gitpod-build-cw-idp.13 |
bd01a4e
to
1f0c626
Compare
@@ -0,0 +1,19 @@ | |||
syntax = "proto3"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, I'd name the file in full - identity-provider. Just because it avoids an extra mental lookup
|
||
service IDPService { | ||
// GetIDToken produces a new IT token | ||
rpc GetIDToken(GetIDTokenRequest) returns (GetIDTokenResponse) {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rpc GetIDToken(GetIDTokenRequest) returns (GetIDTokenResponse) {}; | |
rpc GetToken(GetTokenRequest) returns (GetTokenResponse) {}; |
It's misleading that it says ID, when the request/response objects don't refer to an id
property
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The returned token is an IDToken in the OIDC sense. I added a link to the spec in the comment.
@@ -26,5 +29,11 @@ type Configuration struct { | |||
// Path to directory containing database configuration files | |||
DatabaseConfigPath string `json:"databaseConfigPath"` | |||
|
|||
// RedisAddress configures the redis connection of this component | |||
RedisAddress string `json:"redisAddress"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this a struct, which has an address as a property. I can imagine we may want to pass additional info (like creds) and having these sit under a single redis
property makes it easier.
Redis struct {
Address string `json:...`
}
Server *baseserver.Configuration `json:"server,omitempty"` | ||
} | ||
|
||
type RedisConfiguration struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not anymore :)
} | ||
|
||
if len(req.Msg.Audience) < 1 { | ||
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("must have at least one audience entry")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("must have at least one audience entry")) | |
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Must have at least one audience entry")) |
For consistency with other APIs here
}) | ||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
err := redisClient.Ping(ctx).Err() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't dug into this, but is the Redis client reconnecting? If so, we shouldn't need to do a ping here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just creating the client doesn't check if the connection works. I'd rather the component fails to start than fails to function if Redis is unavailable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but this only checks at connection time (server startup time), not subsequently so the value we get from this remains limited
@@ -114,13 +131,26 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro | |||
|
|||
oidcService := oidc.NewService(cfg.SessionServiceAddress, dbConn, cipherSet, stateJWT) | |||
|
|||
var idpCache idp.KeyCache | |||
if redisClient == nil { | |||
log.Info("No Redis cache configured - caching IDP public keys in memory.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually want to allow this mode? Given we run multiple replicas of pub-api, this is bound to cause issues. I'd rather we say we have a dependency on Redis and we do not try to cache in memory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed it
rootHandler.Mount(idpRoute, idpServiceHandler) | ||
|
||
// IDP well-known and keys routes | ||
rootHandler.Mount("/idp", deps.idpService.Router()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, what stops us serving these on the idpServicehandler
rather than having to have another endpoint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment
@@ -1507,6 +1507,7 @@ export class WorkspaceStarter { | |||
"function:getTeams", | |||
"function:trackEvent", | |||
"function:getSupportedWorkspaceClasses", | |||
"function:getIDToken", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's quite opaque, can we be more specific? Like mentioning IDP?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment
var _ v1connect.IDPServiceHandler = ((*IDPService)(nil)) | ||
|
||
// GetIDToken implements v1connect.IDPServiceHandler | ||
func (srv *IDPService) GetIDToken(ctx context.Context, req *connect.Request[v1.GetIDTokenRequest]) (*connect.Response[v1.GetIDTokenResponse], error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to have a unit test (like the other API packages) for this just to avoid setting the precedent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
f6bb261
to
0f47870
Compare
/hold |
/hold cancel (verified that the Redis readiness probe works and public-api-server starts) |
Use: "aws", | ||
Short: "Login to AWS", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
cmd.SilenceUsage = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check if the aws binary exists to avoid confusing errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking of requesting to install the CLI, but that is just fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could link to the AWS Docs. Arguably though, if someone's using this feature they're deep enough into it that they'll have looked at our (yet to be written) docs which will need to state that the AWS CLI needs to be installed.
/hold cancel Redis isn't a stateful set yet, which means that when Redis gets restarted we loose the keys. That's likely to happen around deployments - and expected. |
/hold Updating the key cache mechanism to make it more resilient against Redis failures |
/hold cancel |
// vault write auth/jwt/login role=demo jwt=$TKN -format=json | ||
out, err := exec.Command("vault", "write", "-format=json", "auth/jwt/login", "role="+idpLoginVaultOpts.Role, "jwt="+tkn).CombinedOutput() | ||
if err != nil { | ||
return fmt.Errorf("%w: %s", err, string(out)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return fmt.Errorf("%w: %s", err, string(out)) | |
return xerrors.Errorf("%w: %s", err, string(out)) |
RunE: func(cmd *cobra.Command, args []string) error { | ||
cmd.SilenceUsage = true | ||
if idpLoginAwsOpts.RoleARN == "" { | ||
return fmt.Errorf("missing --role-arn or IDP_AWS_ROLE_ARN env var") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return fmt.Errorf("missing --role-arn or IDP_AWS_ROLE_ARN env var") | |
return xerrors.Errorf("missing --role-arn or IDP_AWS_ROLE_ARN env var") |
awsCmd := exec.Command("aws", "sts", "assume-role-with-web-identity", "--role-arn", idpLoginAwsOpts.RoleARN, "--role-session-name", fmt.Sprintf("gitpod-%d", time.Now().Unix()), "--web-identity-token", tkn) | ||
out, err := awsCmd.CombinedOutput() | ||
if err != nil { | ||
return fmt.Errorf("%w: %s", err, string(out)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return fmt.Errorf("%w: %s", err, string(out)) | |
return xerrors.Errorf("%w: %s", err, string(out)) |
@@ -114,13 +129,22 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro | |||
|
|||
oidcService := oidc.NewService(cfg.SessionServiceAddress, dbConn, cipherSet, stateJWT) | |||
|
|||
if redisClient == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why we need check here, we have err = redisClient.Ping(ctx).Err()
in above, if redisClient
is nil, I think it will failed or panic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RedisClient is never nil and without the Ping call we'll fail at runtime if redis isn't available. This way the service doesn't start rather than fail strangely when running.
Ah forgot to add hold, but it's small thing |
Description
Private Video - Watch Video
This PR implements an OIDC identity provider, and adds an
IDToken
function to the API surface. This function is served frompublic-api-server
and delegates authZ/authN to server by calling a no-opGetIDToken
function.Private keys used for signing the JWT are only held in memory and are ephemeral. A Redis-backed public key cache ensures that the JSON Web Key Set contains all public keys currently in circulation. The issued ID tokens have a TTL of one hour, as do the public keys whose TTL is refreshed whenever they're used.
The
IDToken
API call is available throughgp idp
which is currently hidden, because this is not a released feature. To this end, the workspace token gained theGetIDToken
function scope.The new code has a test coverage > 70%.
Release Notes
Documentation
Build Options:
Experimental feature to run the build with GitHub Actions (and not in Werft).
leeway-target=components:all
Run Leeway with
--dont-test
Publish Options
Installer Options
Add desired feature flags to the end of the line above, space separated
Preview Environment Options:
If enabled this will build
install/preview
If enabled this will create the environment on GCE infra
Valid options are
all
,workspace
,webapp
,ide
,jetbrains
,vscode
,ssh