AWS Bites Podcast

45. What’s the magic of OIDC identity providers?

If you are thinking of using an external CICD tool to deploy to AWS you are probably wondering how to securely connect your pipelines to your AWS account. You could create a user for your CICD tool of choice and copy some hard coded credentials into it, but, let’s face it: this doesn’t feel like the right - or at least the most secure - approach! In the previous episode we discussed how AWS and GitHub solved this problem by using OIDC identity providers and this seems to be a good solution to the problem. In this episode of AWS Bites we will try to demystify the secrets of OIDC identity providers and explain how they work and what’s the trust model between AWS and an OIDC provider like GitHub actions. We will also explain all the steps required to integrate AWS with GitHub, how JWT works in this particular scenario and other use cases where you could use OIDC providers.

In this episode, we mentioned the following resources:

Let's talk!

Do you agree with our opinions? Do you have interesting AWS questions you'd like us to chat about? Leave a comment on YouTube or connect with us on:

Luciano
00:02:21

Yeah, I'll try my best to do that, but before I do that, there is a link that we are going to have in the show notes, which is actually the GitHub documentation that explains really well how all of that works and there are illustrations. So if everything we say today is not 100% clear, we really recommend to check out this particular article. So the first thing to clarify is that there are two main entities here, and we are talking about AWS itself and GitHub, so we need to figure out how to make them talk to each other and how to create this kind of trust relationship. And in the OpenID Connect lingo, there are two pieces of terminology, identity provider and service provider. In this case, GitHub is the identity provider and AWS is the service provider. So GitHub is kind of the one providing users, while AWS is the one providing a specific service to the user. And this is where I was a little bit confused at first, because I don't think this is the most intuitive use case to understand this difference. In fact, GitHub doesn't really have a concept of users for AWS itself. GitHub doesn't really have a database, let's say, where there is a mapping between particular user names and particular roles. All this stuff actually still lives in AWS. So we'll see how this can be a little bit confusing and hopefully we'll try to explain that a little bit better. And another thing to keep in mind to really understand why there is this little bit of a blurry definition between who is providing the users and who is providing the services is because this is not like a user-facing integration, but it's more of a service-to-service integration. So in a way, we are connecting two services and the definition of a user there is not the canonical one, I would say. So in reality, the way we could see that is that AWS is providing a particular role and AWS is basically trusting GitHub to generate some sort of credential that will allow GitHub to assume that role. And we'll try to explain better how all of that works. So yeah, the first thing that needs to happen in this particular scenario is that we'll need to tell AWS to trust GitHub. So we need to create a trust relationship. And once trust is established, again, GitHub can just say, okay, there is a token that proves that I am the thing you trusted before, now give me access to this particular role. And that access is safer than the permanent credential scenario because that kind of access is using temporary credentials, using STS. So those credential will be short-lived and the chance of leaking them is much, I guess, is much harder to, even if they are leaked, is much harder to take advantage of these credentials, or at least not long-term. Okay. That sounds really good.

00:06:14

And this is actually an interesting thing. I don't know if anyone is familiar with OIDC. It's kind of an extension of OAuth2 that also specifies in a much stricter way how the URL structure should be made, how the tokens should be created. While OAuth2 was much more liberal and every OAuth2 provider could be implemented in a much different way, in OIDC, you literally just need to know that one URL and everything else is standardized. So that's why we can here afford to specify only one particular URL. The other field that we need to populate in this form is something called audience. And audience, I don't think it's extremely important here because I think GitHub action can customize that audience on demand if you want to. But the standard convention that you find in the documentation is to set that to sts.amazonaws.com. And this is basically a value that will be available in your tokens and that you need to check to make sure that GitHub generated the token for the right application, in this case integration with AWS. And then the last thing that you need to do is to pass the time print of the TLS certificate. This is not really something that you need to copy paste, you just need to click a button in the UI and AWS will download the time print of the TLS certificate of the connection to that URL that we specified as the OIDC identity provider. And this is important because we need to make sure that in the future when AWS connects again to GitHub actions, it's still connecting to the same server, so to speak, that the trust is given by the TLS certificate. So if that TLS certificate changes, most likely we want to revisit the trust relationship and make sure we are still talking with the right provider at the other end of the line. Okay, so that's interesting to know.

Luciano
00:08:52

So the next step is to create a trust policy in AWS, and that trust policy needs to have certain particular fields to make sure that you are kind of locking down the security as much as possible. So you're not basically allowing anyone or anything to assume the role. You just want GitHub action and maybe a specific workflow even to assume that particular role. So I suppose at this point you should have in mind exactly the kind of pipeline you're going to build in GitHub and what kind of permission that pipeline would require, and you create this trust policy where you specify that the principal for the trust policy is the ARN of the AM identity provider that we just created. Then the action is STS assume role with web identity, and then we can specify a bunch of conditions. We want to check that the audience is actually the one we specified, STS, amazon.aws.com, but also if we want to lock down the role to a particular GitHub action workflow, we can also specify another condition saying that the subject, which is going to be a field in the token specifying exactly the workflow that triggered that particular execution, matches exactly your expectation. So let's say that you have, I don't know, a project called e-commerce, and that project has a particular repository and a particular workflow in GitHub action called, I don't know, build and publish. You will have a way to say assume this role only if the pipeline was the e-commerce and the workflow was build and deploy. So you can create a condition to limit that kind of thing. At that point you have this trust policy and you can attach... that allows you to assume a role and that role can have specific permissions like, I don't know, you'll be able to create a bucket, you'll be able to deploy a lambda and all the things that you need to do for deploying your application. We mentioned...

Luciano
00:10:59

Yeah, one of the things that we mentioned before is that OIDC standardizes also the format of a token. A token can really be anything, like any string that you can verify and make sure it is actually trustworthy because, I don't know, maybe you can do an API call and get that that token is reliable from the API call or the token itself is somehow signed and you can trust that that signature gives you a guarantee that somebody trustworthy generated that token. In the case of OIDC, this is kind of the choice. They went for signed tokens and the technology of choice is JWT JSON web token. I have written an article a couple of years ago with some illustration that tried to describe in brief what's the structure and how they are generated, how they are validated. We'll have a link to that article if you want to go deeper, but the summary of that is that a JWT token is a string made of three parts separated by a dot and those three parts are a header, a payload, and a signature. They are all encoded in base64url and if you basically split the three parts and do a base64url decode, the header and the payload are actually two JSON objects, JSON encoded objects, and the payload can contain properties that are generally called claims and those properties are... they can be whatever you want, but there are some standards. For instance, audience is one of those, AUD, and generally represents the particular application for which the token was generated. So if you have an identity provider that can generate tokens for multiple applications, you can use the audience to make sure that you are receiving a token that is meant to be used in a particular application.

00:13:02

And if you want to check the signature, you also need to know which key was used to sign the token, so the ID of the key is another field that you will generally find in the payload. And in the case of GitHub Actions, there is also subject which is, in that case, will contain a reference to the workflow that generated the token. So, other information that you can use and is the information we can use in our roles to limit the fact that only the particular workflow can assume a particular role. One interesting thing is that you might wonder how the signature thing works because it's a little bit magic if you never really looked under the hood, but it works by using... Actually, JWT is a little bit open. You can use both symmetric and asymmetric encryption, so you could have either just a shared key to sign the token or you can have public and private key. Of course, in the case of OIDC, you want to have public and private key because you don't want to share a secret key between GitHub and AWS because then it means everyone will know that secret key and everyone will be able to create signed tokens. Instead, when you use a model with asymmetric keys, you will have a public key that allows you to validate tokens and that can be... Anyone can read that. And the private key is only known to GitHub in this case and it means that only GitHub will be able to sign these tokens. So, in reality, you almost never want to use the symmetric key these days. You always go for public and private keys.

00:16:16

Now, this is where it gets a little bit speculative because we'll need to imagine what AWS will do to actually trust that particular token. Because the token, as we understood this like a string, where you can do some decoding and get some information out of it, and then there is a signature that gives some sort of trust that it was generated by a trusted source. So what AWS should do, in my opinion at least, is that first of all, it needs to check if the token is well formed. So is it a valid JWT? Can we decode it? And then are there three parts, a header, a payload, and a signature? Can we read the claims inside the payload? And then when we read the claims, is this token issued by an identity provider that we recognize? So this particular account did have a connection, a trusted relationship with this particular identity provider. If yes, then at that point it needs to check the audience. Like, do we recognize the application for which this token was created? And in our example, we said we will just use the generic STS, AWS, something, I don't even remember. But you can keep that generic or you can customize it if you have different applications. And GitHub Actions can actually change that value for you when you create the token. So that value is actually a little bit of a placeholder that you can configure. Either keep it standard if you have one particular use case, or you can customize it by application. And then the next phase is, okay, once we have validated that the token is correct, that the information in the token looks good and we understand it, we need to make sure that that token is authentic. So it wasn't forged by a third party, but it needs to come really from GitHub Action. And the way that I assume AWS is going to verify that is by using OIDC. So it's going to see in the claims what is the key ID. It knows what's the URL of the token, what's the name of the public key URL endpoint from the OIDC protocol. And it's going to use that to download that particular key, the public key. And then at that point, it can actually check, okay, was it really this key, the one that signed the token? So there is kind of a double trust there. One is given by the fact that we created this trust relationship with that particular URL of the OIDC provider. And the other one is given by the fact that AWS can download a public key from that URL, and that public key actually can verify that the token was signed by that particular OIDC provider. And at that point, if everything is good, STS will do its own thing. It will create temporary credentials, and it will return these temporary credentials that can be used to interact with AWS and will have the policy that is given to the particular role for those credentials. Okay. What form, what does it, what do those credentials look like? What form do they take?

00:20:19

So I don't know if there are interesting examples outside the CI/CD world, but for instance, if you have an on-premise Jenkins and you have your own OIDC provider, you could build basically that integration pretty much the same way as we explained for GitHub Actions. AWS is just going to trust your own on-premise identity provider to basically generate tokens that then will give access to particular roles on AWS. But I was thinking also, can you use this for other workflows? I don't know if it's the best way of doing this, but technically you could use event-driven things if you want to basically, I don't know, maybe a physical action in the real world triggers something in AWS. I'm thinking, I don't know, maybe you have an application that every time you enter the office, you swipe your own card to track time or something like that. If there is, I don't know, I know OIDC provider connected there, that's OIDC provider. There could be an application that creates a token using that OIDC provider, assumes a role, and then maybe recording a DynamoDB table that, I don't know, somebody accesses the building at a certain point. So you could create this kind of, I suppose, actions where you have a source of authentication and you want to assume a role, but in a kind of time-limited fashion. Now, probably there are better ways to implement this kind of stuff, but I was trying to stretch my imagination on once you understand this integration, how much can you use it? How far can you go? All right.

Eoin
00:21:50

It sounds like that this is applicable in any case where you've got a system to system interaction between a non-AWS environment and an AWS environment. So it could be used when you've got an on-prem Sys application that needs to talk to an AWS application, for example, and you don't want to have access keys configured. We know that in EC2 or in ECS, you've got like a profile you can associate with that resource. So you can use that resource. So you don't have to have secret keys, but outside of AWS, it has been very common for people to just use long-lived keys to perform that kind of interaction. So I guess this is one way of overcoming that. You just need to think about what is your OIDC provider and how are you going to issue those credentials? I know some people would maybe integrate it into Active Directory and have some sort of service credentials. So that might be another way of doing that. It might be worthwhile mentioning actually, as a slight segue, there was a very recent announcement for a new feature called IAM roles anywhere, and we can link to this announcement in the show loads, but it sounds like another way of doing this kind of system to system interaction where instead of having an OIDC provider, you use a public key infrastructure. So PKI, you've got a root certificate authority yourself, or you can use AWS certificates manager, and you can issue client certificates, and you actually set up a trust chain between your certificate authority and AWS, and then use client certificates as a means to exchange them for temporary credentials. So it's slightly tangent, but related, and it's a very recent announcement, so I just thought I'd call it out there. But I guess what this is kind of saying to us is that all you need to do is to create something that follows the OIDC protocol, and you can pretty much use it to exchange identities for credentials in AWS. So I guess that means it's potentially something that could cause security issues if you don't get it right. Now you could create your own identity provider. You could use it to give administrator access to all of your accounts. So you need to understand exactly what the trust model is. Is it worthwhile maybe summarizing that? How would you describe the trust model for this OIDC relationship?

Help us to make this transcription better! If you find an error, please submit a PR with your corrections.