{"id":44971,"date":"2023-03-21T12:25:54","date_gmt":"2023-03-21T19:25:54","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=44971"},"modified":"2024-12-13T14:16:50","modified_gmt":"2024-12-13T22:16:50","slug":"securing-containers-with-rootless","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/securing-containers-with-rootless\/","title":{"rendered":"Secure your .NET cloud apps with rootless Linux Containers"},"content":{"rendered":"<blockquote>\n<p>This post was <strong>updated on April 12, 2024<\/strong> to reflect the latest releases.<\/p>\n<\/blockquote>\n<p>Starting with <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-8-preview-1\/#net-container-images\">.NET 8<\/a>, all of our Linux container images will <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/pull\/4397\">include a non-root user<\/a>. You&#8217;ll be able to host your .NET containers as a non-root user with one line of code. This platform-level change will make your apps more secure and .NET one of the most secure developer ecosystems. It is a small change with a big impact for <a href=\"https:\/\/en.wikipedia.org\/wiki\/Defense_in_depth_(computing)\">defense in depth<\/a>.<\/p>\n<p>This change was inspired by our earlier project enabling <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-6-is-now-in-ubuntu-2204\/#net-in-chiseled-ubuntu-containers\">.NET in Ubuntu Chiseled containers<\/a>. Chiseled (AKA &#8220;distroless&#8221;) images are intended to be appliance-like so non-root was an easy design choice for those images. We realized that we could apply the non-root feature of Chiseled containers to all the container images we publish. By doing that, we&#8217;ve raised the security bar for .NET container images.<\/p>\n<p>This post is about the benefit of non-root containers, workflows for creating them, and how they work. A follow-on post will discuss how to best use these images with Kubernetes. Also, if you want a simpler option, you should check out <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-builtin-container-support-for-the-dotnet-sdk\/\">built-in container support for the .NET SDK<\/a>.<\/p>\n<blockquote>\n<p>Note: <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-chiseled-containers\/\">.NET Chiseled images<\/a> are now GA.<\/p>\n<\/blockquote>\n<h2>Least privilege<\/h2>\n<p>Hosting containers as non-root aligns with <a href=\"https:\/\/en.wikipedia.org\/wiki\/Principle_of_least_privilege\">principle of least privilege<\/a>. It&#8217;s free security provided by the operating system. If you run your app as <code>root<\/code>, your app process can do anything in the container, like modify files, install packages, or run arbitrary executables. That&#8217;s a concern if your app is ever attacked. If you run your app as non-root, your app process cannot do much, <em>greatly<\/em> limiting what a <a href=\"https:\/\/seclists.org\/oss-sec\/2019\/q1\/119\">bad actor could accomplish<\/a>.<\/p>\n<p>Non-root containers can also be thought of as contributing to <a href=\"https:\/\/en.wikipedia.org\/wiki\/Digital_supply_chain_security\">secure supply chain<\/a>. Most of the time, people talk about secure supply chain in terms of blocking bad dependency updates or <a href=\"https:\/\/en.wikipedia.org\/wiki\/Software_supply_chain\">auditing component pedigree<\/a>. Non-root containers come after those two topics. If a bad dependency slips through your process (and there is a probability that one will), then a non-root container may be your best last defense. <a href=\"https:\/\/kubernetes.io\/docs\/concepts\/security\/pod-security-standards\/#restricted\">Kubernetes hardening best practices<\/a> require running containers with a non-root user for this same reason.<\/p>\n<h2>Meet <code>app<\/code><\/h2>\n<p>All of our Linux images &#8212; starting with .NET 8 &#8212; will contain an <code>app<\/code> user. The <code>app<\/code> user will be able to run your app, but won&#8217;t be able to delete or change any of files that come with the container image (unless you explicitly allow that). The naming is appropriate since the user can do little more than run your app.<\/p>\n<p>The user <code>app<\/code> isn&#8217;t actually new. It is the same one we&#8217;re using for <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/6f8e695e4c0fe2708980217bc2ab2dbda68a1f79\/src\/runtime-deps\/6.0\/jammy-chiseled\/amd64\/Dockerfile\">our Ubuntu Chiseled images<\/a>. That&#8217;s a key design point. Starting with .NET 8, all of our Linux container images will contain the <code>app<\/code> user. That means that you can switch between the images we offer, and the user and uid will be the same.<\/p>\n<p>I&#8217;ll describe the new experience in terms of the <code>docker<\/code> CLI.<\/p>\n<p>Meet <code>app<\/code>.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm mcr.microsoft.com\/dotnet\/aspnet:8.0 cat \/etc\/passwd | tail -n 1\r\napp:x:1654:1654::\/home\/app:\/bin\/sh<\/code><\/pre>\n<p>That&#8217;s the last line of the <a href=\"https:\/\/www.redhat.com\/sysadmin\/linux-user-group-management\"><code>\/etc\/passwd<\/code> file<\/a> in our images. That&#8217;s the file that Linux uses for managing users.<\/p>\n<p>We <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/issues\/4693#issuecomment-1612002902\">selected a uid just above 1000<\/a> to avoid <a href=\"https:\/\/en.wikipedia.org\/wiki\/User_identifier#Reserved_ranges\">reserved ranges<\/a>. We also decided that this <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/issues\/4083\">user should have a home directory<\/a>.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm -u app mcr.microsoft.com\/dotnet\/aspnet:8.0 bash -c \"cd &amp;&amp; pwd\"\r\n\/home\/app<\/code><\/pre>\n<p>We looked around a bit and discovered that Node.js, Ubuntu 23.04+, and Chainguard are all on this same plan. Nice!<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm node cat \/etc\/passwd | tail -n 1\r\nnode:x:1000:1000::\/home\/node:\/bin\/bash\r\n$ docker run ubuntu:noble cat \/etc\/passwd | tail -n 1\r\nubuntu:x:1000:1000:Ubuntu:\/home\/ubuntu:\/bin\/bash\r\n$ docker cp $(docker create --name cg cgr.dev\/chainguard\/dotnet-runtime):\/etc\/passwd .\/passwd &amp;&amp; docker rm cg &amp;&amp; cat .\/passwd | tail -n 1 &amp;&amp; rm .\/passwd\r\nnonroot:x:65532:65532:Account created by apko:\/home\/nonroot:\/bin\/sh<\/code><\/pre>\n<p>The last one is <a href=\"https:\/\/github.com\/chainguard-images\">Chainguard<\/a>. Those images are structured differently (for good reason), so a different pattern was used. It is fine for everyone to create their own users, however, it is best to avoid matching UIDs.<\/p>\n<p>There are <a href=\"https:\/\/gist.github.com\/richlander\/6fbe3d741a3d6ed023db8f2d767f9efd\">lots of users in container images<\/a>, but <a href=\"https:\/\/en.wikipedia.org\/wiki\/Nobody_(username)\">none of them are considered appropriate for this use case<\/a>. It would be nice to <a href=\"https:\/\/github.com\/alpinelinux\/docker-alpine\/issues\/232\">reduce the number of users<\/a>, however, that&#8217;s unlikely to happen and one of the benefits of using distroless\/Chiseled images.<\/p>\n<p>Windows Containers already <a href=\"https:\/\/learn.microsoft.com\/virtualization\/windowscontainers\/manage-containers\/container-security\">have a non-admin capability<\/a>, with the <code>ContainerUser<\/code> user. We opted against adding <code>app<\/code> to our Windows Container images. You should follow Windows Team guidance on how to best secure Windows Container images.<\/p>\n<h2>Using <code>app<\/code><\/h2>\n<blockquote>\n<p>&#8220;Non-root-capable&#8221;: Configure your container as non-root with a one-line <code>USER<\/code> instruction.<\/p>\n<\/blockquote>\n<p><a href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#user\">Docker<\/a> and <a href=\"https:\/\/kubernetes.io\/docs\/tasks\/configure-pod-container\/security-context\/\">Kubernetes<\/a> make it easy to specify the user you want to use for your container. It&#8217;s a one-liner. Per our definition, &#8220;non-root-capable&#8221; means you can switch to non-root as a one-liner. That&#8217;s very powerful, since the ease of a one-liner removes any reason to not run more securely.<\/p>\n<p>Note: <code>aspnetapp<\/code> is used throughout as a substitute for your app.<\/p>\n<p>You can set the user via the CLI with <code>-u<\/code>.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm -u app mcr.microsoft.com\/dotnet\/runtime-deps:8.0 whoami\r\napp<\/code><\/pre>\n<p>Specifying the user via the CLI is fine, but more for testing or diagnostic scenarios. It is best for production apps to <a href=\"https:\/\/docs.docker.com\/develop\/develop-images\/dockerfile_best-practices\/#user\">define the <code>USER<\/code> in a Dockerfile<\/a>, with the username or uid.<\/p>\n<p>As the user:<\/p>\n<pre><code class=\"language-dockerfile\">USER app<\/code><\/pre>\n<p>As the UID:<\/p>\n<pre><code class=\"language-dockerfile\">USER 1654<\/code><\/pre>\n<p>Via an <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/issues\/4506\">environment variable for the UID<\/a>.<\/p>\n<pre><code class=\"language-dockerfile\">USER $APP_UID<\/code><\/pre>\n<p>We consider this pattern a best practice because it makes it obvious which user you are using, avoids duplicated magic numbers, and uses a UID, all of which work well if you are using Kubernetes. We&#8217;ll have a post on non-root hosting with Kubernetes shortly.<\/p>\n<p>The following command demonstrates the value of the environment variable.<\/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\"\r\n1654<\/code><\/pre>\n<p>If you don&#8217;t do anything, everything will be the same as before and your image will continue to run as <code>root<\/code>. We hope you take the extra (small) step and run your container as the <code>app<\/code> user. You might be wondering why we didn&#8217;t switch to the non-root user by default. That will be covered in a later section.<\/p>\n<h2>Switching to port <code>8080<\/code><\/h2>\n<p>The biggest sticking point of the project was the ports that we expose. In fact, it is so much of a sticking point that we had to make a <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/whats-new\/dotnet-8\/containers#non-root-user\">breaking change<\/a>.<\/p>\n<p>We decided to standardize on port <code>8080<\/code> for all container images going forward. This decision was based on our earlier experience with Chiseled images, which already <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/6f8e695e4c0fe2708980217bc2ab2dbda68a1f79\/src\/runtime-deps\/6.0\/jammy-chiseled\/amd64\/Dockerfile#L50\">listen on port <code>8080<\/code><\/a>. All the images now match.<\/p>\n<p>However, ASP.NET Core apps (using our .NET 7 and earlier container images) <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/305864588999a50a2112ed1ac3c39f51c04f37d7\/src\/runtime-deps\/6.0\/bullseye-slim\/amd64\/Dockerfile#L19\">listen on port <code>80<\/code><\/a>. The problem is that <a href=\"https:\/\/www.w3.org\/Daemon\/User\/Installation\/PrivilegedPorts.html\">port 80 is a privileged port<\/a> that requires <code>root<\/code> permission (at least in some places). That&#8217;s inherently incompatible with non-root containers.<\/p>\n<p>You can see how the ports are configured in our images.<\/p>\n<p>For .NET 8:<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm mcr.microsoft.com\/dotnet\/aspnet:8.0 bash -c \"echo \\$ASPNETCORE_HTTP_PORTS\"\r\n8080<\/code><\/pre>\n<p>For .NET 7 (and earlier):<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm mcr.microsoft.com\/dotnet\/aspnet:7.0 bash -c \"echo \\$ASPNETCORE_URLS\"\r\nhttp:\/\/+:80<\/code><\/pre>\n<p>Note: We also changed the environment variable we use to set the port. More on that shortly.<\/p>\n<p>Going forward, your port mapping will need to change. You can do this via the CLI. You&#8217;ll need <code>8080<\/code> on the right-hand of the mapping. The left-hand side can match or be another value.<\/p>\n<pre><code class=\"language-bash\">docker run --rm -it -p 8000:8080 aspnetapp<\/code><\/pre>\n<p>Some users will want to continue to use port 80 (and <code>root<\/code>). You can still do that.<\/p>\n<p>You can re-define <code>ASPNETCORE_HTTP_PORTS<\/code> in your Dockerfile or via the CLI.<\/p>\n<p>For Dockerfile:<\/p>\n<pre><code class=\"language-dockerfile\">ENV ASPNETCORE_HTTP_PORTS=80<\/code><\/pre>\n<p>For Docker CLI:<\/p>\n<pre><code class=\"language-bash\">docker run --rm -e ASPNETCORE_HTTP_PORTS=80 -p 8000:80 aspnetapp<\/code><\/pre>\n<p>.NET 8 Windows Container images use port <code>8080<\/code> as well.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm mcr.microsoft.com\/dotnet\/aspnet:8.0-nanoserver-ltsc2022 cmd \/c \"set | findstr ASPNETCORE\"\r\nASPNETCORE_HTTP_PORTS=8080<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/pull\/44194\"><code>ASPNETCORE_HTTP_PORTS<\/code><\/a> is a new environment variable for specifying the port (or ports) for ASP.NET Core (actually, <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/fundamentals\/servers\/kestrel\">Kestrel<\/a>) to listen on. It takes a semi-colon delimited list of port values. .NET 8 images use this new environment variable, instead of <code>ASPNETCORE_URLS<\/code> (which is used in .NET 6 and 7 images). <code>ASPNETCORE_URLS<\/code> remains a useful advanced feature. It enables specifying both raw HTTP and TLS ports in one configuration and overrides both <code>ASPNETCORE_HTTP_PORTS<\/code> and <code>ASPNETCORE_HTTPS_PORTS<\/code>.<\/p>\n<h2>Non-root in action<\/h2>\n<p>Let&#8217;s take a look at what non-root looks like from a few different angles so that you can better understand what&#8217;s actually going on. I&#8217;m using Ubuntu 24.04.<\/p>\n<p>You can use our <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/aspnetapp\/\">aspnetapp<\/a> sample to try this scenario yourself. It configures the container to always run as <code>app<\/code>.<\/p>\n<pre><code class=\"language-bash\">$ pwd\r\n\/home\/rich\/git\/dotnet-docker\/samples\/aspnetapp\r\n$ cat Dockerfile | tail -n 2\r\nUSER $APP_UID\r\nENTRYPOINT [\".\/aspnetapp\"]\r\n$ docker build --pull -t aspnetapp -f Dockerfile .<\/code><\/pre>\n<p>Let&#8217;s see if we can observe the user in action, which has been set in the <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/tree\/main\/samples\/aspnetapp\/Dockerfile\">Dockerfile<\/a> we&#8217;re using.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm --name aspnetapp -d -p 8000:8080 aspnetapp\r\n5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad\r\n$ curl http:\/\/localhost:8000\/Environment\r\n{\"runtimeVersion\":\".NET 8.0.4\",\"osVersion\":\"Debian GNU\/Linux 12 (bookworm)\",\"osArchitecture\":\"Arm64\",\"user\":\"app\",\"processorCount\":8,\"totalAvailableMemoryBytes\":4113694720,\"memoryLimit\":0,\"memoryUsage\":34250752,\"hostName\":\"ead528d37b0e\"}\r\n$ docker exec aspnetapp ls -l\r\ntotal 200\r\n-rw-r--r-- 1 root root   154 Feb 22 05:46 appsettings.Development.json\r\n-rw-r--r-- 1 root root   151 Jul 11  2023 appsettings.json\r\n-rwxr-xr-x 1 root root 72544 Apr 12 17:11 aspnetapp\r\n-rw-r--r-- 1 root root   457 Apr 12 17:11 aspnetapp.deps.json\r\n-rw-r--r-- 1 root root 61952 Apr 12 17:11 aspnetapp.dll\r\n-rw-r--r-- 1 root root 44696 Apr 12 17:11 aspnetapp.pdb\r\n-rw-r--r-- 1 root root   469 Apr 12 17:11 aspnetapp.runtimeconfig.json\r\ndrwxr-xr-x 5 root root  4096 Apr 12 17:11 wwwroot\r\n$ docker top aspnetapp  \r\nUID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD\r\n1654                7833                7815                0                   17:13               ?                   00:00:00            .\/aspnetapp<\/code><\/pre>\n<p>Notice the <code>user<\/code> property in the JSON content returned from <code>Environment<\/code> endpoint, above.<\/p>\n<p>You can see that application is running as <code>app<\/code> and that the files are owned by <code>root<\/code>. That means that the application files are protected from being altered by this user.<\/p>\n<p>From <a href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#copy\">Dockerfile reference<\/a>:<\/p>\n<blockquote>\n<p>All new files and directories are created with a UID and GID of 0, unless the optional <code>--chown<\/code> flag specifies a given username, groupname, or UID\/GID combination to request specific ownership of the copied content<\/p>\n<\/blockquote>\n<p>Let&#8217;s try some rootful actions on this container, using <code>docker exec<\/code> on the same container.<\/p>\n<pre><code class=\"language-bash\">$ docker exec aspnetapp rm aspnetapp.pdb\r\nrm: can't remove 'aspnetapp.pdb': Permission denied\r\n$ docker exec aspnetapp touch \/file\r\ntouch: \/file: Permission denied\r\n$ docker exec aspnetapp which dotnet\r\n\/usr\/bin\/dotnet\r\n$ docker exec aspnetapp rm \/usr\/bin\/dotnet\r\nrm: can't remove '\/usr\/bin\/dotnet': Permission denied\r\n$ docker exec aspnetapp apt-get update &amp;&amp; apt-get install -y curl\r\nReading package lists...\r\nE: List directory \/var\/lib\/apt\/lists\/partial is missing. - Acquire (13: Permission denied)\r\n$ docker exec aspnetapp sudo apt-get update &amp;&amp; sudo apt-get install -y curl\r\nOCI runtime exec failed: exec failed: unable to start container process: exec: \"sudo\": executable file not found in $PATH: unknown<\/code><\/pre>\n<p>The result: <code>Permission denied<\/code> and <code>sudo<\/code> isn&#8217;t present to work around that. That&#8217;s what we want. Let&#8217;s try again, but elevate to <code>root<\/code>.<\/p>\n<pre><code class=\"language-bash\">$ docker exec -u root aspnetapp bash -c \"rm aspnetapp.pdb &amp;&amp; ls aspnetapp.pdb\"\r\nls: aspnetapp.pdb: No such file or directory\r\n$ docker exec -u root aspnetapp bash -c \"touch \/file &amp;&amp; ls \/file\"\r\n\/file\r\n$ docker exec -u root aspnetapp bash -c \"rm \/usr\/bin\/dotnet &amp;&amp;  ls \/usr\/bin\/dotnet\"\r\nls: \/usr\/bin\/dotnet: No such file or directory\r\n$ docker exec -u root aspnetapp bash -c \"apt-get update &amp;&amp; apt-get install curl\"   \r\nGet:1 http:\/\/deb.debian.org\/debian bookworm InRelease [151 kB]\r\nGet:2 http:\/\/deb.debian.org\/debian bookworm-updates InRelease [55.4 kB]\r\nGet:3 http:\/\/deb.debian.org\/debian-security bookworm-security InRelease [48.0 kB]\r\nGet:4 http:\/\/deb.debian.org\/debian bookworm\/main arm64 Packages [8685 kB]\r\nGet:5 http:\/\/deb.debian.org\/debian bookworm-updates\/main arm64 Packages [12.5 kB]\r\nGet:6 http:\/\/deb.debian.org\/debian-security bookworm-security\/main arm64 Packages [147 kB]\r\nFetched 9099 kB in 2s (4099 kB\/s)\r\nReading package lists...\r\nReading package lists...\r\nBuilding dependency tree...\r\nReading state information...\r\nThe following additional packages will be installed:\r\n  krb5-locales libbrotli1 libcurl4 libgssapi-krb5-2 libk5crypto3 libkeyutils1\r\n  libkrb5-3 libkrb5support0 libldap-2.5-0 libldap-common libnghttp2-14 libpsl5\r\n  librtmp1 libsasl2-2 libsasl2-modules libsasl2-modules-db libssh2-1\r\n  publicsuffix\r\n...\r\ncurl 7.88.1 (aarch64-unknown-linux-gnu) libcurl\/7.88.1 OpenSSL\/3.0.11 zlib\/1.2.13 brotli\/1.0.9 zstd\/1.5.4 libidn2\/2.3.3 libpsl\/0.21.2 (+libidn2\/2.3.3) libssh2\/1.10.0 nghttp2\/1.52.0 librtmp\/2.3 OpenLDAP\/2.5.13\r\nRelease-Date: 2023-02-20, security patched: 7.88.1-10+deb12u5\r\nProtocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp\r\nFeatures: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd<\/code><\/pre>\n<p>We can now kill the container.<\/p>\n<pre><code class=\"language-bash\">$ docker kill aspnetapp\r\naspnetapp\r\n$ docker ps\r\nCONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES<\/code><\/pre>\n<p>You can see that <code>root<\/code> is able to do a lot more, in fact, anything it wants. After <code>curl<\/code> is installed into the Debian image, an attacker could start executing scripts from any webserver they choose. In the case of Alpine, it ships with <code>wget<\/code>, which removes a step in that chain.<\/p>\n<p>Surely, the answer is to remove the <code>root<\/code> user to avoid these risks. No. In fact, removing the <code>root<\/code> user has undefined behavior. The best option is to run as a non-root user. It removes a whole class of attacks via well-defined mechanisms.<\/p>\n<p>The use of <code>docker exec -u root<\/code> might seem scary. If an attacker can run <code>docker exec -u root<\/code> on your running container, then they already have access to the host, and you&#8217;re already in far more trouble than anything that is addressed by this post.<\/p>\n<p>What about <code>sudo<\/code>? <code>sudo<\/code> isn&#8217;t included in our images and never will be.<\/p>\n<h2>Chiseled<\/h2>\n<p>We have a <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/822166bbf88e23cbbbba6fedc3f56b99276a1e96\/samples\/aspnetapp\/Dockerfile.chiseled\">similar sample<\/a> that is hosted at <code>mcr.microsoft.com<\/code>. It is more limited in what it can do, by design, by virtue of being based on an <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-chiseled-containers\/\">Ubuntu Chiseled image<\/a>.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm -d --name aspnetapp -p 8000:8080 mcr.microsoft.com\/dotnet\/samples:aspnetapp-chiseled \r\n1d2120f991562e51b65dd09fba693a68f6230e914c75e21a93811219bb52c16c\r\n$ curl http:\/\/localhost:8000\/Environment \r\n{\"runtimeVersion\":\".NET 8.0.4\",\"osVersion\":\"Ubuntu 22.04.4 LTS\",\"osArchitecture\":\"Arm64\",\"user\":\"app\",\"processorCount\":8,\"totalAvailableMemoryBytes\":4113694720,\"memoryLimit\":0,\"memoryUsage\":37683200,\"hostName\":\"1d2120f99156\"}\r\n$ docker kill aspnetapp\r\naspnetapp<\/code><\/pre>\n<p>This image also uses the <code>app<\/code> user.<\/p>\n<h2>Hosting in Azure container services<\/h2>\n<p>It is straightforward to adopt this pattern in Azure container services. There are two aspects to consider, the port and the user.<\/p>\n<p>Some container services offer a higher-level experience than Kubernetes and require a different configuration option.<\/p>\n<ul>\n<li>Azure App Service requires <a href=\"https:\/\/learn.microsoft.com\/azure\/app-service\/configure-custom-container#configure-port-number\"><code>WEBSITES_PORT<\/code><\/a> to use a port other than port 80. It can be set via the CLI or in the portal.<\/li>\n<li>Azure Container Apps enables <a href=\"https:\/\/learn.microsoft.com\/azure\/container-apps\/get-started-existing-container-image-portal?pivots=container-apps-public-registry\">changing the port as part of resource creation<\/a>.<\/li>\n<li>Azure Container Instances enables <a href=\"https:\/\/learn.microsoft.com\/azure\/container-instances\/container-instances-quickstart-portal\">changing the port as part of resource creation<\/a>.<\/li>\n<\/ul>\n<p>None of those services offer an obvious way to change the user. If you set the user in your Dockerfile (which is a best practice), then there is no need for that capability.<\/p>\n<p>We&#8217;ve started to spread the word about this change to other clouds.<\/p>\n<h2>Next steps<\/h2>\n<p>The next step is to investigate the cases where non-root could be a challenge, such as for diagnostic scenarios. Some of the examples use <code>docker exec -u root<\/code>. That works well in a local environment, however <code>kubectl exec<\/code> <a href=\"https:\/\/github.com\/kubernetes\/kubernetes\/issues\/30656\">doesn&#8217;t offer a user argument<\/a>. We&#8217;ll look more deeply at Kubernetes workflows with non-root in a later post.<\/p>\n<p>We&#8217;re also going to continue working with container hosting services to ensure that .NET developers can move to .NET 8 container images with ease, particularly those providing higher-level experiences like Azure App Service.<\/p>\n<h2>Summary<\/h2>\n<p>A key part of our mission on the .NET Team is defense in depth. Everyone needs to think about security, however, we are in the business of closing off whole classes of attack with a single change or feature. To be true, we could have made this change when we started publishing container images about a decade ago. We have been asked for non-root guidance and non-root container images for many years. It honestly wasn&#8217;t clear to us how to approach that, in large part because the pattern we&#8217;re now using didn&#8217;t exist when we started out. There wasn&#8217;t a leader in safe container hosting for us to learn from. It was the experience of working with Canonical on chiseled images that enabled us to discover and shape this approach.<\/p>\n<p>We hope that this 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 about patterns for securing your containers with a non-root user, and changes to .NET container images in .NET 8 to enable this behavior.<\/p>\n","protected":false},"author":1312,"featured_media":44972,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7237,7720,326],"tags":[7701,57,92],"class_list":["post-44971","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-containers","category-linux","category-security","tag-dotnet-8","tag-containers","tag-linux"],"acf":[],"blog_post_summary":"<p>Learn about patterns for securing your containers with a non-root user, and changes to .NET container images in .NET 8 to enable this behavior.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/44971","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=44971"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/44971\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/44972"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=44971"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=44971"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=44971"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}