 Welcome to Checking the Chains at the Gate. We're going to talk about how we can build supply chain security policies using Gatekeeper and a project called Ratify. My name is Jeremy. This is probably my favorite contribution to Kubernetes. It's my cat with a laser background. He was the 120 release mascot. I was the 120 release lead. So that was the fun perk you get for being a release lead. You get to come up with a fun mascot. I'm also a chair in SIG Release, and I'm also on the Kubernetes Code Conduct Committee. So you may know me from one of those things. In my day job, I am an engineer on the Azure Container Upstream Team, and we are really focused on making sure that open source and Microsoft have a good time together, specifically around containers. We're looking at things in the supply chain security space and making sure that we are doing good things with open source and helping open source be successful as well. Before we get going, let's kind of take a moment to pause and reflect on the state of building and shipping services and software today. It's pretty good. There's a lot of tools that we can use, a lot of languages, a lot of frameworks that we can take dependencies on to just satisfy the needs and allow us to focus on business use cases. We don't have to go and reinvent the wheel all the time. Sometimes we do, but not all the time. And generally, it just makes things faster. The downside to that is that there's a lot of things. There's a lot of complexity to worry about. Building services is easier than it probably has been in a long time, but operating those services and knowing what's running in different places is a little more complicated than it used to be. As we start building microservices and shipping things to more clusters and different clouds, it's really difficult to kind of understand, what am I doing in Cloud A and Cloud B? What's this U I've got between those things? How do I keep track? How do I enforce that we're doing things consistently across those things? And to make matters worse, a lot of governments are helping us by making new regulations and directives that we have to comply with. The EU, this is an example, has mandated that people start to produce S-bombs. The US government has done the same thing. We at Microsoft have had to react and start to build S-bombs for Windows. So think about the complexity that comes into that. And all of that happens on top of the existing complexities and the difficulties we have with everything that we already have to deal with. And to make this problem better, we've added new tools. So instead of just that one S-bomb thing, maybe we've got two tools or we've got a series of frameworks that are coming to help us take that requirement and turn it into reality because things don't magically appear by themselves. How do we take all that stuff and actually make it useful? If we're just building an S-bomb and storing it somewhere, does it really satisfy that requirement we saw earlier? That requirement was we need to produce S-bombs. We need to produce bills of materials so that customers can have better visibility into what we're building. They can make decisions. They can have a better understanding of what vulnerabilities might be in that software. And if we're just producing this thing and then maybe sticking it on a website or sticking it in a SharePoint site or a ShareDrive, is that useful? I think for these things to become the tool that they're intended to be, we really need to start looking at automation and figuring out how we can make automated decisions about these things. Maybe the S-bomb declares that there is something bad in it, and we no longer want to use that. We need to be able to do that in an automated way, not with a Excel spreadsheet that has 7,000 tabs for all the clusters that we're running. So with that in mind, how do we enforce things in clusters? How do we define policies and control what we're going to do? So we're at KubeCon. Let's scope this down to what we can do within our Kubernetes clusters themselves. Things like our cloud provider services, those are out of our control. But we can control what's in our clusters. So what do we do? How do we approach this? If you're using Kubernetes 126 or later, like the cell admission policies might be a really good way for you to start to build policies and apply those to your clusters. You can declaratively make policies and to say, I want this pod to have certain labels or things can only come from certain registries. It's really useful to declaratively say how you want things to look coming into the cluster. But those things are limited because they're running in process. You're limited in what you're able to do. You can't reach out to external data sources. You can't do really complicated things with them. So an answer to that might be to use admission controllers. And admission controllers have been around for quite a while. If you haven't heard the term before, what they basically allow you to do is to hook into the flow. So you type kubectl apply or kubectl create for a resource. It's going to go through a flow. And controllers can plug into two spots within that and they can change the request. So you can mutate your pod definition maybe to rewrite to a different registry or to set a default number of replicas or add labels, maybe billing information for a team that's going to deploy. And then you could also validate those requests. So as it comes in, you can say, you're deploying out of a GCR registry. We don't want to use public internet. Let's block that. So it gives you that kind of hook to be able to do that. But the downside is that you now have to write or run some component in your cluster as well that is just another piece of complexity. All these things, we're going to work on evolving to satisfy these requirements. But there's going to be some trade-offs. So are there pieces of software that we can use to plug into those spots? And it turns out there are. There's a bunch of really great projects in the ecosystem. But we're going to focus on OPA gatekeeper and start from there. There are alternatives. You can take a look at Kiverno or Sigstor's policy controller. But OPA, I think, gatekeeper is a really great project. And it has a cool story that we're going to see unfold through this. So how does gatekeeper work in that scenario? So gatekeeper is based off of Open Policy Agent. And Open Policy Agent uses a declarative language called Rego. So you can write policies in Rego and then have that applied in either of those phases of the emission flow into this cluster. And that works by introducing a couple of new concepts. So we have constraint templates and constraints. And you can think of these as new resources that gatekeeper is going to add into your cluster that allow you to define those policies and then apply those policies to certain sets of resources. So let's take a look at a constraint template first. The constraint template allows us to define parameters, what things we want to include in our policy, what data do we want to use as configuration. So we've got a schema here that defines a parameters field with a label underneath of that that's going to be an array of strings. So in this case, we're going to look at how we can apply required labels or required labels on things coming into the cluster. So our parameter is probably going to be an array of those required labels. I think that is pretty straightforward. Once we have that, then we are going to define the actual policy itself. So in this case, we're going to take that object that's coming in that's under review. We're going to get all of the labels out of it. So in a Kubernetes resource object, there's always a metadata section. Metadata section has labels. So you can write a pretty standard line here that says, in that object, give me all of the labels from the metadata section. Once you have that, you can get that parameter that we defined, which is going to be those required labels. And you can compute the difference. So if we take the required and the provided and there's any difference, then we can generate a status message or an error that says, you did not provide all of the labels that are required. And then Gatekeeper can evaluate that policy and say, I don't want to admit this into the cluster because it's going to evaluate that. So once you've defined the template, then you can define an actual constraint. So we want to apply this to namespaces. So our use case here is all the namespaces in our cluster have to have a set of required labels. In this case, it's just going to be Gatekeeper, simple. But it's going to match anything that's coming in that's creating any namespace. OK, so let's see what that looks like in action. So we're going to, oops, did not play. It was? Oh, OK, there it goes, cool. It's not showing up on my speaker view, that's why. OK, so we have that constraints template and the constraint itself defined. We're going to apply those into the cluster next. So we're going to use kubectl to do that. And that'll be in just a second. So once we've applied that, can we make a namespace? That's kind of the question that we're going to answer. So we're going to apply those things. We're going to do our kubectl apply with the constraint template and the constraint. That's going to configure Gatekeeper. Gatekeeper's already been loaded into the cluster behind the scenes. Not super useful. Now we can use kubectl apply to try to create that namespace itself, and we can see we get an error back that we don't have the required labels. We have to provide the label Gatekeeper. So that's pretty nice, right? We can apply that policy, and it's good to go. But as we're starting to think about policies, especially in the supply chain space, I think things are going to change over time. If you've ever run a vulnerability scanner on something, today is going to be different than what you get tomorrow. So a useful feature might be to not just look at things that are coming into the system, but maybe audit things that are in the cluster already. That's something you're not going to be able to do with something that doesn't know how to do auditing, but Gatekeeper luckily does. So Gatekeeper allows us to do auditing. So we can apply our policies to the cluster, and if auditing is enabled, then it's going to go through on a periodic basis and look and see how do the things that are in the cluster now line up with what's defined in the policy. So we'll get an audit timestamp. We can see when the last audit happened and kind of use that in two ways. One, we can look and see, do we need to make tweaks to our policies and will things get blocked that we don't want to get blocked? But also it'll give us a good awareness of how things have drifted from what that policy might look like. So in the case of our namespaces, this is my kind cluster, and I applied that policy after the kind cluster came up. And obviously, kind is not going to add the Gatekeeper label to all of the namespaces that I have in the cluster. So here we can see that all of those existing namespaces now would violate that policy and would either need to be remediated or we'd need to tweak the policy to exclude a few things. So that's pretty cool, I think. I think you can get to this point where you're labeling things automatically, you're enforcing that policy that you have decided makes sense for you. But that's really working on things that are in the cluster. And I think for our supply chain use cases, that's going to be a little hard to do. Are we going to take all of our S-bombs and load them into the cluster as some custom resource? That's probably not going to happen. So how do we deal with things that are not in the cluster themselves? And what kind of use cases do we think might be necessary there? So maybe we want to validate the integrity of images. We want to make sure that the image that we pushed as part of our release pipeline is what we thought it was at the time that it was published. So maybe we want to do verification of the images. Maybe we want to take those S-bombs and block deployments based off of their contents. Maybe we know that Log4j was used back in 2021 and it had a bad version. How can we use an S-bomb to remediate that situation? So maybe we want to do that. And then finally, maybe we want to leverage vulnerability scans. Maybe we have a nightly process that goes through and scans all of the images that we are using in production and attaches a Sarah report out of trivia or something that has all the vulnerabilities that have been identified. And then we want to block anything or maybe audit everything that has critical vulnerabilities that align with our compliance requirements. So to do that, we're obviously going to need some pieces of external data. Those aren't going to be defined in the pod spec. They're not going to be part of that core Kubernetes resource that's going to be part of the admission flow. So to do that, we already know we're using Gatekeeper. And luckily, Gatekeeper has this really cool feature called external data. So your scenario is probably like this. You have a cluster. Gatekeeper is running inside of it. And an admission request comes in. In that admission request, there's some data that you can make decisions on. But now we want to go and do those external cool supply chain security workflows. So what we need to do is figure out how to plug that into Gatekeeper. And Gatekeeper does that with external data. And you do that by creating and registering providers in your cluster. So provider is going to be that specific piece of functionality that knows how to go and do something else aside from what's in that request. It might query a service or it might pull some data down from somewhere else, from your secure data location source that's off to the side. So what will happen is when that request comes in, you've defined a policy that says, use external data to get an answer based off of the data that I'm going to send to you. So the flow looks kind of like that. Your request will go out to the provider. And then the provider is going to talk to your external data source and then say, yes, this is good or no, this is bad. It's going to make that decision and return that back to Gatekeeper so they can evaluate the rest of the policy and figure out what needs to happen from there. And that looks pretty similar to any other policy. You just need to specify that you're going to use external data, which provider you want to use. So you can have multiple of these things set up and then what you want to send to it. So we're going to, in this case, grab all of the containers and images in the containers from this template. So this is probably a deployment or something that has a deployment template inside of it. Once it gets all those images back, it's going to send that to the external data provider. The external data provider will do the thing that it needs to do and return you back an answer or response. And then you can take that response and figure out, was this successful? Was it a failure? What do I do in the next step? And then code all of that into your policy. OK, so now we know we're going to use Gatekeeper. We know that external data allows us to reach out and touch some other service. This may be in the cluster. But how do we get those things? How do we distribute those S-bombs? How do we distribute the signatures, the vulnerability reports, or anything else? This is a space that's going to keep emerging and changing. And new things are going to unfold all the time. So I think one way would be to put those into file service. You could maybe put them in blob storage in your cloud provider and query things and find them. That's a fine way. But how do you associate those things with the images that are coming in? So you are going to deploy your application. It's got two containers. Those two containers have S-bombs associated with them or signatures. How do you know which one is which? Well, in the new version of OCI, 1.1, which is going to be approved hopefully soon, there's a new API called Refers. And Refers allows you to say, Object A, Object B, have a relationship, and then you can kind of query that directed graph. And a fun fact is that you can use OCI registries to store other things. So it's not just images. If you're using Helm, maybe you've pulled Helm charts from an OCI registry. Flux does that as well. You can store Flux things and pull them from OCI registries. If you use SigStore, they are now storing signatures in registries. When you co-sign something, the signature ends up in the registry. And you can find it that way. The Refers API takes that to the next level so you can store all kinds of things and associate them together. So you get this nice graph of, say, Object, maybe my container, Sbomb, and maybe a signature. And the way that that ends up working is that you get, hopefully, in the registry near you soon, you get your image. So we've made this image index that has an ANB64 image for our internal application. And we then create a new artifact, just another thing that we're going to store in the registry for our Sbomb. There we go. So we use an image manifest to create that new Sbomb. The really, really critical thing for this workflow to work, though, is that subject block that's at the bottom. That subject block is going to refer back to the thing that it's describing or that it's related to. So if it's a signature, that signature is going to refer back to the container. It could be a signature that refers back to an Sbomb. The really cool thing that you can do here is that you can have that graph walk down several levels. And we'll see that in just a second. It's going to refer back to that. And then the Refers API will allow you to do those queries and say, for this given image, which is shot2564413, oh, sorry, shot256892053, et cetera, give me all of the objects that refer to that. And it'll give you back that whole collection. And you can filter it down later. And you can say, give me all of the Sbombs that refer to that and pull those down as well. So we know that OCI is going to be useful here. Let's take a look at maybe a notional application, how this might work for folks. And this is going to be a really simple one single container application. But we've decided that we are going to sign it. And we're going to produce an Sbomb. And we want to publish that Sbomb. So it's going to be that kind of complete workflow. And to do that, we've enabled our teams through a reusable GitHub workflow that installs all the tools and then signs with Cosign. It also signs with a new tool called Notary or Notation that allows us to do slightly different things. And then we're going to generate an Sbomb and attach that to the thing as well. So it should end up with an Sbomb and two signatures. And what that'll look like in the registry once we've published that is a little something like this. OK, there it goes. It's playing. Cool. So we're going to look at the registry first. So I have an ACR instance. I snagged kubecton.eu. I couldn't believe that that was not taken. To not, I have kubecton.eu.azurecr.io. Super cool. And in that registry, we can see there are three things. The first one is our image. And then the second two are the signatures. The .sig is the Cosign signature. It uses their tag kind of triangulation to figure out how to relate things together. And the second one is the Notary image. And it's using the referrers API. So now we're going to use a tool called oros to kind of look at what that tree looks like. I said it's a directed graph. Let's see what that kind of looks like. So we're going to use the oros discover tool and use the image. We know that it's the Shah 0, or sorry, D09925. So cool. We see the Sbomb and the signature. Those are both of the things that are attached with the OCI spec. So now that we have that thing published, and we kind of know the structure, we know that we're using OCI artifacts to relate those things together, how do we write policies for that? Well, we know we're going to use gatekeeper. We know we're going to use external data. So we're going to need a provider. We're going to write our policies to send images to that provider. That provider is going to do the things that it needs to do with our supply chain security artifacts. But what's that provider going to be? Do we have to write that ourselves? And the answer is no. There is a project called Radify that is specifically built to be an external data provider for gatekeeper that knows how to do things with OCI artifacts and refers and supply chain security artifacts. It's really basic in the sense that it is just a binary. And you use this binary locally. It's pretty cool. You can validate things on your machine. You can test configurations without having to have a Kubernetes cluster, without having to have gatekeeper set up in that whole workflow. You can have an image, and you can validate that image with Radify and configure the policies how you need them, and then deploy that to your cluster. It also runs as an HTTP service so that you can plug into that gatekeeper external data flow. But it works pretty well. And the way that it works is providing a framework and then a set of pluggable verifiers. And the verifiers have a really simple interface. Can I verify this thing? And then verify this thing. And they get, you can see in the signatures, these OCI spec references. So it's going to get a descriptor. And the descriptor is going to have the media type, or the artifact type. And then it's going to have the referrer, or the subject, of the thing that it's referring to. So it can go and do that kind of triangulation and figure out what it needs to do and pull the things down. So you can build a ton of cool functionality into these plugins. Gatekeeper, or sorry, Radify has a few of these already, and we'll take a look at them now. So first, we said that we signed those images with cosine. So let's use this cosine verifier. So we can see in plugins under verifier, we have cosine, right? It's going to apply to vend dev cosine artifacts.sig version 1. And then we make a policy that says we want to apply this to any of those objects. So anything that's in the request that has that type, we're going to apply this to it. You could write some filtering and customization there as well to tailor it down, but we're going to apply it to everything. And let's take a look what that looks like. So first, we're going to use cosine to verify that this is actually a valid image. So we're going to use the cosine tool. We're going to give it that reference and configure it. This is using brand new version of cosines. I have to give it a couple of extra options. But when we do that, we want to assert that this thing was signed by our GitHub workflow. And we use that as the authority that we want to sign against or verify against. And that worked. So that's cool. We know that the image signature was valid. It's successful. Now we would expect Radify using that other configuration to do the same thing for us behind the scenes. So we're going to run the Radify command directly. You can see that. And it returns back that verification report. So we can see is successful true. In our policy, we're probably going to write some rules to say, if the verification result is true, go ahead and send that across. Now we can do the same thing with Gatekeeper. In this case, we'll use notation just to show the difference. But here, again, we have an external data policy. It's going to use that external data provider, which is Radify. We're going to send it the images. And then we're going to apply it to pods in this case. So like the last one we saw, we used namespaces. We wanted to verify the labels. In this case, we're going to use pods. So we want to make sure that all the pods that are coming in in the default namespace have signatures associated with them. And that looks a little something like this. So we're going to take our existing application and then some random internet thing we pulled down from web and networks. We're going to try to run those things. So we'll Coup Ctl run that first. And we can see that the first one was rejected because verification failed. That image was not successfully passed through our policy. The second one is going to be that image that we published to kubekan.eu.azurecr.io that went through our whole release workflow and has all of the signatures and stuff attached to it. So here we go. We're going to run that. Oh, I spelled something wrong, so I have to go back and fix the spelling. Even in recorded demos, I messed up. OK, so it's going through that workflow now. And cool, we see pod created. It did not fail. It didn't get rejected by that workflow. So those are two real simple use cases, right? Those are built-in functionalities that ratify has by default. What if we want to do something more complex? What if we want to wrap a salsa verifier? We produce salsa providence for our build. We want to verify that before we admit things into the cluster. Or maybe we want to use triby to scan an image. Or maybe we want to parse through the serif result from a previous triby run. Maybe we want to invoke some artisanal tools we have internally or we have some government-compliance things that we need to run and validate before we can finish deploying into a cluster. And the way we're going to do that is by building a custom verifier plugin. So those things are built-in, the cosine verifier, the notation verifier. What we're going to do now is take a look at what it looks like to build and deploy a custom verifier. So in this case, what we want to do is block licenses that we know are bad for our business, maybe like AGPL. And then we also know that there are certain packages, log for J as a good example, that we just don't want to allow into the cluster because we know they're bad. So let's go take a look at what that looks like. I'm going to jump out of here. And I'm going to change my display settings just so it looks better for everybody. OK, so in VS Code, what we're going to do is take a look at, if I can find VS Code. Yeah, here we go. I'm trying to find VS Code. There we go. OK, so let's make that bigger. So what the plugins do is implement a really simple interface. They are implementing that, can verify and verify those functionalities. But in the end, they're really just binaries. So they're a binary that ratify will know how to load because you're going to give it this configuration. So we have a name for the verifier that we want to use, the things that we want to verify against. So just like in that config file we saw for the cosine verifier, we said that we wanted to use that cosine signature format. In this case, we're going to use application slash spdxjson because that's what we've published the sbombs as. And then we said that we wanted to use certain licenses as things we want to disallow. And then we want to have certain packages as things we want to disallow in the cluster. So we'll provide those things as parameters here. That's what that spec configuration is going to look like. But the really, really cool thing in here is that we specify what the source for this plugin is. And that plugin itself can also be an OCI artifact, which could have signatures on it, could have sbombs on it. So you can extend this out all the way through that kind of long tail of this whole workflow. So what we're going to do is we're going to build and publish a plugin. We have one written already. It's pretty simple. We'll take a look at it. But we're going to publish it to this ACR instance, which is the same one we've been pulling our images from. And then we're going to register it in the cluster using this CRD definition. So we're going to create a new verifier. Ratify will take it and run with it. What that thing looks like, obviously, we want to define what the parameters look like here, that kind of schema. So we've got a pretty simple set of structs in here, our plugin config. It has a name. It's got disallowed licenses, which is just a slice of strings. And then some packages, which is a slice of a package type that has a version and a name. So pretty basic configuration. And then in the main, the important things to see in here are that we're going to do that verify reference method. That's the contract that we have to satisfy. And at the bottom, we're going to just basically loop through and look for all of the bad packages. And if we find those bad packages, we're going to generate an error message or a status message to send back to Ratify. So basic simple code. Let's take a look at what building that looks like. So I'm going to go back over here. Here's my package or my project, right? So I've got my YAML file that we're going to deploy in just a second. I've got my code. Let's build it. Let's see if this works. I like doing live things. OK, so we're going to build this on a Mac, obviously. So I need to make sure this is a Linux image, because I was going to build it as a Darwin binary by default. And we're going to disable Cgo because this is going to run a distrelas container later. OK, cool. That's done. So now what we're going to do is or us publish that thing. And we don't need that command on the end or that artifact type. So what we're going to do is or us push this thing to that URL, that registry, version 0.0 alpha 0. Oops, I spelled or us push. Run that again. Spell that ratify. OK, so now we're using the or us command to push that, just like if we're going to do a Docker push. This is the same kind of command. It just knows how to work with pretty much any generic artifact that you want to push to a registry. And this is running a little slow because I'm on Wi-Fi. But cool, it worked, right? So we have that in the registry now. We could do an or us discover on that thing just to see if it's there. Cool, and then we could do or us pull on that, just like if we're going to pull a Docker container. Same basic principle. Cool, so it downloads that. Now we have ratify package checker. It's good to go. It'll take a second to download. OK, so now let's jump to one more tab real quick. And let's take a look what's in our cluster. So we've got a kind cluster here. We have gatekeeper. What's running in that namespace? We have gatekeeper already. And we have ratify running. That's great. So let's tail the logs on ratify. So we've got our ratify instance running. That's great. So what we're going to do now is cancel that download. I'm just going to take a little bit. And we're going to do kubectl apply package verifier. Cool, it created a new verifier for us in the cluster. And we can see that it is responding to that already. It resolved the manifest for it. And it's downloading the plug-in just like I was doing with or us poll. It's going to do the exact same thing into the cluster. It might take a little bit of time to download because the download speed is not great on this Wi-Fi connection. So let me just switch kubectl context back to a different cluster. Just a demo magic here. We're going to switch back to our coupon demo cluster. And here we're going to look in that directory. And we'll see, hey, all of our things are there. So that's great. What we're going to do now is check that. Let's get the verifiers. And cool, we see our notary verifier, which is the default one is there. And then our new package checker one is there as well. Perfect. So we should be able to run an image, our image, and see if it validates that or not. So let's do that. Back to this project. We're going to run this command right here, kubectl demo. And we're going to run that image that we built and published as part of our build pipeline. OK, so clear that out. OK, so kubectl run, we're going to name it demo. And then again, the same image we've been talking about through this workflow. And when I run that, you can take a look at the logs and see what's happening in the ratify. Oh, it worked. Ah, it failed to download the plugin because it's trying to redownload it because the ratify is too slow in here. So boo, that failed. We can see that it is trying to use the plugin to pull, using that verifier config, to pull the plugin down, store it inside of the ratify pod that's running, and then use that verifier to admit or deny the workload. In this case, because it failed to download the plugin, it wasn't able to apply the plugin, and it didn't fail for us. But let's see if the other cluster had that. Same problem. OK, let's get our pods again. OK, I think it's still downloading. That's a bummer. OK, so if we try to run this again, I think we'll see the same behavior where it's going to allow it. Oh, no, there we go. Oh, we have a different policy. That's great. That's unexpected. OK, so let's take a step back and look at the config real quick. We can see that's still downloading. It's a bummer. OK, so in VS Code again, the verifier result is going to either return false for a successful, and then the list of error messages that we expect to see come back. So disallowed licenses, disallowed packet versions, if it matches any of those things. Otherwise, it's going to successfully validate things and return those back in. The configuration itself, again, we're going to use that OCI artifact that we pushed to the registry. We can see it pulling it and downloading that inside of the ratified pod itself. And then the configuration that we want to pass into it. So you can write pretty complicated things here. If you needed to combine different things, you could actually compose those plugins together to make different things. But I think the really interesting thing is you can make multiple verifiers. So as policies or needs evolve, and as compliance requirements evolve and change, you'd be able to either consume existing verifiers and add them into your configuration and evolve that as necessary, or write things to satisfy your own business needs as you go through things. OK, I'll jump back to my slides real quick, because I've got one thing of notes that you can take a look at. All right, so what we saw today were a few things from Gatekeeper. So I've included some links to that Gatekeeper documentation. Also a link to the ImageSpec and OCI. So if you're interested in learning more about what's changing in the OCI 1.1 spec, you can take a look at things there. If you're interested in writing your own plugins for Ratify, there's a great link to some documentation here. I used Azure Container Registry for most of this demo, but you could totally use another CNCF project called ZOT. ZOT has implemented the OCI 1.1 spec, and they were super quick about updating it. So as new RCs come out for the OCI 1.1 spec, they're implementing that, and you can totally take advantage of all of these great new capabilities that are in the OCI spec. Finally, I use the ORUS CLI tool. So if you want to take a look at that, you can go to orus.land, and it'll give you all the cool information about that. And then if you want to take a look at my demo, I uploaded it to GitHub, so github.com slash my name slash ratify package checker. And you can download the source and check it out there. Cool. If you want to rate this session, please do so. There is a QR code here that you can scan. You can provide feedback, and we greatly appreciate it. And thanks to our sponsors for recording this and sending it out, and thank you for attending. I think we're out of time, so I'll stay over here if you have questions. You want to come over and ask anything. Feel free to ping into the channel in the CNCF Slack as well if you want to have any kind of discussion there later on. Just feel free to app me at my name, Jeremy Rickard, and then I'll see your message in there. We can continue the conversation.