Netflix / bless

Repository for BLESS, an SSH Certificate Authority that runs as a AWS Lambda function
Apache License 2.0
2.73k stars 223 forks source link

Support authentication with OpenID Connect #93

Open stoggi opened 5 years ago

stoggi commented 5 years ago

Hello,

I'm interested in authenticating users to the BLESS lambda using OpenID Connect identity tokens.

This would be an alternative to using KMS auth to prove the identity of the caller. Users could use AWS AssumeRoleWithWebIdentity API to get temporary credentials to invoke the lambda, and then pass their identity token in the payload to get a certificate signed for a username that matches a claim in the identity token.

I'm keen to implement this feature. Is there any interest and support for new features like this?

russell-lewis commented 5 years ago

Can you elaborate a bit more on the system you're thinking about? That might help to see if anyone else has interest.

At least in my experience, anywhere we have OIDC clients set up, it is from code running in containers or instances doing code flow. That same application would then be able to just sign certificates directly using the SSO session directly, instead of stitching SSO AuthN->AWS AssumeRole->Invoke Lambda->Re-AuthN user.

You can always use the bless.ssh module directly if you need a python library that can issues certs. See https://github.com/Netflix/bless/blob/master/bless/aws_lambda/bless_lambda_user.py#L162-L186

stoggi commented 5 years ago

I want to let engineers sign their public SSH keys from their own machines in a similar setup as Lyft with https://github.com/lyft/python-blessclient. But engineers also have federated access to AWS from an identity provider and so there are no users in IAM to use with KMS Auth.

One way to federate AWS access is to use a command line application running on an engineers laptop that uses OpenID Connect Authorization Code flow with PKCE. The users browser authenticates to their identity provider with MFA, and securely redirects the identity token (and refresh token) to the command line application. With the identity token they can then call AssumeRoleWithWebIdentity to get temporary AWS access keys. For subsequent use, the command line application would use the refresh token stored in the keychain to retrieve a new identity token, until the refresh token expires or is revoked.

This method of assuming a role in AWS is neat because AWS can verify the validity of the identity token, and there is no need to host a publicly facing API gateway. It is also a good way to transfer an identity token from the users browser into their keychain using a well known OAuth2.0 flow with PKCE.

The piece that is missing is getting the BLESS lambda to also validate the identity token was signed by the correct identity provider and has the correct username claim for the requested user. Eventually I would like to be to support adding other principals in the certificate based on claims in the identity token, for example to add a sudo principal and use https://github.com/uber/pam-ussh for sudo access on the host.

I did play around with using KMS Auth with the assumed role, since you can include the subject identifier in an IAM policy as a condition to encrypt the KMS key:

"Condition": {
    "StringEquals": {
        "kms:EncryptionContext:from": "${accounts.google.com:sub}"
    }
}

However, this limits my host usernames to the subject identifiers from the identity provider. It would be nice to verify a different claim in the identity token, or support other features like sudo principals.

russell-lewis commented 5 years ago

Running a PKCE enabled client on dev machines makes perfect sense in that case, as you'll have the Access token, ID Token, and Refresh Token directly. It sounds reasonable to use other claims to control which principals can be present in the cert.

Every time I take a closer look at OIDC ID tokens, I find conflicting information on how they should be used vs. how they are often used. For example, many ID Providers don't allow ID Tokens to be refreshed. So I'm not sure if your environment could refresh the ID token without a reauth flow.

Our stance internally has generally been to only use Access Tokens for the case you are describing (where a client app is calling a remote resource). Then have the remote resource (e.g. BLESS) validate the access token either directly (in the case they are also JWTs) or from calling /userinfo with the access token. Assuming a valid access token from a valid issuer with valid scopes, the claims or /userinfo response could could control the cert contents.

However, it looks like API_AssumeRoleWithWebIdentity takes ID Tokens (they only accept 2 ID Providers for Access Tokens). Assuming a valid ID token from a valid issuer with the expect audience (your PKCE app's client id), it seems reasonable that the claims could control the cert contents. Assuming of course your ID Token has all the claims you need.

Given the inconsistencies between ID Providers and contents of Access Tokens and ID Tokens, as well as the bias towards making as few external calls from the Lambda as possible, I'd suggest building general JWT validation logic. That way you could configure a schema for additional JWT payload structure validation, and that schema could support Access tokens, ID tokens, or custom tokens.

Lastly, take a look at https://github.com/Netflix/bless/blob/master/bless/cache/bless_lambda_cache.py and you could extend that to cache the JWT signing key.

stoggi commented 4 years ago

@russell-lewis thank you for your detailed answer above. I had a go at JWT validation for BLESS #100

I opted to allow the user to hardcode their JSON Web Key (JWK) instead of fetching it at runtime. For OpenID Connect you could find this key in your .well-known/openid-connect endpoint. But I think this strikes a good balance between following the OpenIDConnect standard, and verifying generic JWTs. Maybe a future PR could let you specify the URL and cache it.

For example, with Google you can fetch the jwks_uri in: https://accounts.google.com/.well-known/openid-configuration