{"id":45310,"date":"2023-04-17T10:48:59","date_gmt":"2023-04-17T17:48:59","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=45310"},"modified":"2024-12-13T14:16:23","modified_gmt":"2024-12-13T22:16:23","slug":"running-nonroot-kubernetes-with-dotnet","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/running-nonroot-kubernetes-with-dotnet\/","title":{"rendered":"Running non-root .NET containers with Kubernetes"},"content":{"rendered":"<blockquote>\n<p>This post was updated on <strong>April 25, 2024<\/strong> to reflect the latest releases.<\/p>\n<\/blockquote>\n<p>Rootless or non-root Linux containers have been the most requested feature for the .NET container team. We recently announced that all <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/securing-containers-with-rootless\/\">.NET 8 container images will be configurable as non-root<\/a> with a single line of code. This change is a welcome improvement in security posture. In that last post, I promised a follow-up on how to approach non-root hosting with Kubernetes. That&#8217;s what we&#8217;ll cover today.<\/p>\n<p>You can try hosting a non-root container on your cluster with our <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/kubernetes\/non-root\/README.md\">non-root Kubernetes sample<\/a>. It is part of a larger set of <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/kubernetes\/README.md\">Kubernetes samples<\/a> we&#8217;re working on.<\/p>\n<p>This post will help you follow <a href=\"https:\/\/kubernetes.io\/docs\/concepts\/security\/pod-security-standards\/#restricted\">Kubernetes &#8220;Restricted&#8221; hardening best practices<\/a>. Non-root hosting is a key requirement of the guidance.<\/p>\n<h2><code>runAsNonRoot<\/code><\/h2>\n<p>Most of what we&#8217;ll be discussing relates to the <a href=\"https:\/\/kubernetes.io\/docs\/reference\/generated\/kubernetes-api\/v1.24\/#securitycontext-v1-core\"><code>SecurityContext<\/code><\/a> section of a Kubernetes manifest. It holds security configuration that is applied by Kubernetes.<\/p>\n<pre><code class=\"language-yml\">    spec:\n      securityContext:\n        runAsNonRoot: true\n      containers:\n      - name: aspnetapp\n        image: mcr.microsoft.com\/dotnet\/samples:aspnetapp-chiseled\n        ports:\n        - containerPort: 8080<\/code><\/pre>\n<p>This <code>securityContext<\/code> object in this example validates that all the containers in the pod will be run with a non-root user. It really is as simple as that.<\/p>\n<p>You can see this in a broader context in <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/3e8c396639c30387237fc898d9152858b479a458\/samples\/kubernetes\/non-root\/non-root.yaml#L15-L16\">non-root.yaml<\/a>.<\/p>\n<p><code>runAsNonRoot<\/code> tests that the user (via UID) is a non-root user (<code>&gt; 0<\/code>), otherwise pod creation will fail. Kubernetes only reads container image metadata for this test.  It doesn&#8217;t read <code>\/etc\/passwd<\/code> since that would require launching the container (which defeats the purpose of the test). That means that <code>USER<\/code> (in a Dockerfile) must be set by UID. It will fail if <code>USER<\/code> is set by name.<\/p>\n<p>We can simulate this same test with <code>docker inspect<\/code>.<\/p>\n<pre><code class=\"language-bash\">$ docker inspect mcr.microsoft.com\/dotnet\/samples:aspnetapp-chiseled -f \"{{.Config.User}}\"\n1654<\/code><\/pre>\n<p>As you can see, our sample image sets the user by UID. However, <code>whoami<\/code> will still report the user as <code>app<\/code>, since it is natural for it is running on a live operating system.<\/p>\n<p><code>runAsUser<\/code> is a related setting, although not used in the example above. <code>runAsUser<\/code> should only be used if <code>USER<\/code> in the container image is unset, set by name rather than UID, or otherwise needs to be changed. We&#8217;ve made it very easy to use the new <code>app<\/code> user as a UID, such that <code>runAsUser<\/code> should not be commonly needed for .NET apps.<\/p>\n<h2><code>USER<\/code> best practices<\/h2>\n<p>We recommend using the following pattern for setting <code>USER<\/code> in a Dockerfile.<\/p>\n<pre><code class=\"language-dockerfile\">USER $APP_UID<\/code><\/pre>\n<p>The <code>USER<\/code> instruction is often placed just before the <code>ENTRYPOINT<\/code>, although the order doesn&#8217;t matter.<\/p>\n<p>This pattern results in the <code>USER<\/code> being set as a UID, while avoiding magic numbers in your Dockerfile. The environment variable already defines the UID value as declared by the .NET image.<\/p>\n<p>You can see the environment variable set in .NET images.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm -u app mcr.microsoft.com\/dotnet\/runtime-deps:8.0 bash -c \"echo \\$APP_UID\"\n1654<\/code><\/pre>\n<p>Our sample Dockerfile <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/6f32b916ff36e8ff8e3f22b79c0978e4a9392c59\/samples\/aspnetapp\/Dockerfile#L21\">set the user by UID<\/a>. As a result, it works well with <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/3e8c396639c30387237fc898d9152858b479a458\/samples\/kubernetes\/non-root\/non-root.yaml#L16\"><code>runAsNonRoot<\/code><\/a>.<\/p>\n<h2>Non-root hosting in action<\/h2>\n<p>Let&#8217;s take a look at the experience of non-root container hosting using our <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/kubernetes\/non-root\/README.md\">non-root Kubernetes sample<\/a>.<\/p>\n<p>I&#8217;m using <a href=\"https:\/\/docs.docker.com\/desktop\/kubernetes\/\">Docker Desktop<\/a> for my local cluster, but any Kubernetes-compatible environment should work well with <a href=\"https:\/\/kubernetes.io\/docs\/tasks\/tools\/\">kubectl<\/a>.<\/p>\n<pre><code class=\"language-bash\">$ kubectl apply -f https:\/\/raw.githubusercontent.com\/dotnet\/dotnet-docker\/main\/samples\/kubernetes\/non-root\/non-root.yaml\ndeployment.apps\/dotnet-non-root created\nservice\/dotnet-non-root created\n$ kubectl get po\nNAME                           READY   STATUS    RESTARTS   AGE\ndotnet-non-root-5b7fc97df7-476s9   1\/1     Running   0          13s<\/code><\/pre>\n<p>The pod was correctly deployed, since the status us reported as <code>Running<\/code>.<\/p>\n<p><code>kubectl<\/code> reports that <code>runAsNonRoot<\/code> was set.<\/p>\n<pre><code class=\"language-bash\">$ kubectl get pod dotnet-non-root-5b7fc97df7-476s9 -o jsonpath=\"{.spec.securityContext}\" \n{\"runAsNonRoot\":true}<\/code><\/pre>\n<p>Now onto using the app, first by creating a proxy to the service.<\/p>\n<pre><code class=\"language-bash\">$ kubectl port-forward service\/dotnet-non-root 8080\nForwarding from 127.0.0.1:8080 -&gt; 8080\nForwarding from [::1]:8080 -&gt; 8080<\/code><\/pre>\n<p>We can now call the endpoint, which also reports the user as <code>app<\/code>.<\/p>\n<pre><code class=\"language-bash\">% curl http:\/\/localhost:8080\/Environment\n{\"runtimeVersion\":\".NET 8.0.4\",\"osVersion\":\"Ubuntu 22.04.4 LTS\",\"osArchitecture\":\"Arm64\",\"user\":\"app\",\"processorCount\":8,\"totalAvailableMemoryBytes\":4113563648,\"memoryLimit\":0,\"memoryUsage\":34082816,\"hostName\":\"dotnet-non-root-8467576789-9dj8g\"}<\/code><\/pre>\n<p><code>user<\/code> is displayed as <code>app<\/code>, as expected.<\/p>\n<p>If we tried to deploy an image that used <code>root<\/code> as the user, with <code>runAsNonRoot<\/code> set to <code>true<\/code>, we would see the following error.<\/p>\n<pre><code class=\"language-bash\">$ kubectl apply -f non-root.yaml\ndeployment.apps\/dotnet-non-root created\nservice\/dotnet-non-root created\n$ kubectl get po\nNAME                            READY   STATUS                       RESTARTS   AGE\ndotnet-non-root-6df9cb77d8-74t96   0\/1     CreateContainerConfigError   0          5s<\/code><\/pre>\n<p>With a bit of digging, we&#8217;d find the reason.<\/p>\n<pre><code class=\"language-bash\">$ kubectl describe po | grep Error\n      Reason:       CreateContainerConfigError\n  Warning  Failed     7s (x2 over 8s)  kubelet            Error: container has runAsNonRoot and image will run as root (pod: \"dotnet-non-root-6df9cb77d8-74t96_default(d4df0889-4a69-481a-adc4-56f41fb41c63)\", container: aspnetapp)<\/code><\/pre>\n<p>With the tour over, we can delete the resources.<\/p>\n<pre><code class=\"language-bash\">$ kubectl delete -f https:\/\/raw.githubusercontent.com\/dotnet\/dotnet-docker\/main\/samples\/kubernetes\/non-root\/non-root.yaml\ndeployment.apps \"dotnet-non-root\" deleted\nservice \"dotnet-non-root\" deleted<\/code><\/pre>\n<h2><code>dotnet-monitor<\/code><\/h2>\n<p><a href=\"https:\/\/github.com\/dotnet\/dotnet-monitor\"><code>dotnet-monitor<\/code><\/a> is a diagnostic tool for capturing diagnostic artifacts from running applications. We offer a <a href=\"https:\/\/hub.docker.com\/_\/microsoft-dotnet-monitor\/\">dotnet\/monitor<\/a> container image for it. Does it work well with non-root hosting? Yes.<\/p>\n<p>The <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/kubernetes\/dotnet-monitor\/README.md\"><code>dotnet-monitor<\/code><\/a> sample demonstrates both ASP.NET and <code>dotnet-monitor<\/code> running as non-root.<\/p>\n<h2>Summary<\/h2>\n<p>You can switch to non-root hosting in Kubernetes with a few straightforward configuration changes. Your app will be more secure and more resilient to attack. This approach also brings your app into compliance with Kubernetes Pod hardening best practices. It&#8217;s a small change with a big impact for defense in depth.<\/p>\n<p>We hope that our container security initiative enables the entire .NET container ecosystem to switch to non-root hosting. We&#8217;re invested in .NET apps in the cloud being high-performance and safe.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to run .NET containers in Kubernetes as a non-root user.<\/p>\n","protected":false},"author":1312,"featured_media":51617,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7509,7237,2904,7720,326],"tags":[7701,89,7730,7729],"class_list":["post-45310","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-aspnetcore","category-containers","category-docker","category-linux","category-security","tag-dotnet-8","tag-kubernetes","tag-non-root","tag-rootless"],"acf":[],"blog_post_summary":"<p>Learn how to run .NET containers in Kubernetes as a non-root user.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/45310","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/1312"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=45310"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/45310\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/51617"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=45310"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=45310"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=45310"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}