NET Web App Host 10.0.113
Overview
This guide covers launching and using the .NET Web App Host 10.0.113 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud app stacks and runtimes listing on AWS Marketplace. You pick it as the software version when you subscribe and launch.
The image is a ready-to-run host for ASP.NET Core web applications, not a bare runtime. The .NET SDK and the matching shared runtime are installed. A real ASP.NET Core application is already compiled on the image and serving under Kestrel, and nginx sits in front of it as the only service on the network.
You build and publish your own application on the instance with the same SDK, and swap it in with one command.
What you get:
- HTTPS from the first boot. nginx serves the app on port 443 with a certificate generated on your instance's first boot, and port 80 redirects to HTTPS. No certificate or key is baked into the image.
- A per-instance admin password. Every path except the
/healthprobe needs HTTP Basic authentication asadmin. The password is generated on first boot and stored as a bcrypt hash for nginx, and in plain text only in a file that root can read. No two instances share one, and one command replaces it. - Loopback only, behind nginx. Kestrel listens on
127.0.0.1:5000. nginx answers only for your instance's own host names and hands the app the configured host name, never the one a client sends. It overwritesX-Forwarded-ForandX-Real-IPwith the real client address. The sample app trusts forwarded headers from127.0.0.1only. - Host from configuration. The app's public URL is set at first boot from the instance's EC2 public DNS name, or from a name you choose. A forged
Hostheader can therefore never point absolute links and redirects elsewhere. - Sandboxed and fail-closed. The app runs as the unprivileged
aspnetuser under a locked-down systemd unit, and neither the app nor nginx can reach the instance metadata service. Neither starts until first boot has generated the certificate and the password. - Your apps on their own disk.
/srv/aspnetis a dedicated 20 GiB EBS volume that holds the published app, its source, the Data Protection key ring and logs. You can resize it independently of the operating system disk. The app creates the Data Protection key ring on your instance, and none ships in the image. - .NET from the Ubuntu archive. This is the MIT-licensed build that Canonical ships in Ubuntu's
maincomponent, soaptand unattended upgrades keep it patched. It runs on a fully patched Ubuntu 24.04 LTS as of build time. - .NET SDK 10.0.113 with the ASP.NET Core and .NET runtimes 10.0.13, in
/usr/lib/dotnet, with thedotnetcommand on every user's PATH. .NET 10 is a long-term support release, supported until November 2028. .NET CLI telemetry is switched off for every user. - A sample app,
SacAppHost, published in/srv/aspnet/app, with its source in/srv/aspnet/src/SacAppHost. - Units and helpers. The
aspnet-app.service,dotnet-firstboot.serviceandnginx.serviceunits, and the helpersaspnet-app-deploy,dotnet-rotate-passwordanddotnet-set-hostname. - Base system. Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu
Architecture: x86_64
Recommended instance type: m5.large
.NET and ASP.NET are trademarks of the Microsoft group of companies. SecureAnyCloud is not affiliated with, endorsed by, or sponsored by Microsoft.
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair, and a VPC subnet with a route to the internet (or a private route you use for management)
- A security group that allows TCP 22 (SSH) from your own networks only, and TCP 443 (HTTPS) and TCP 80 (redirects to HTTPS) from the clients your app serves
Nothing else needs to be open, because Kestrel listens on loopback only.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick .NET Web App Host 10.0.113 on Ubuntu 24.04 as the software version and select your Region. Then choose Launch through EC2.
Choose m5.large, or a larger type for heavier apps, along with your key pair and a security group as described above. The image attaches its 20 GiB app volume automatically. You can make the volume bigger at launch.
To launch with the AWS CLI instead, replace the values in angle brackets:
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=dotnet-host}]'Connect to your instance
The SSH login user for .NET Web App Host 10.0.113 on Ubuntu 24.04 is ubuntu.
From your own machine, connect with:
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot generates the certificate, the app's configuration and the admin password. Then the app and nginx start. Check that both are running:
systemctl is-active aspnet-app nginx
sudo test -f /var/lib/sac/data/dotnet-firstboot.done && echo FIRSTBOOT_DONE dotnet --list-sdks dotnet --list-runtimesactive active FIRSTBOOT_DONE 10.0.113 [/usr/lib/dotnet/sdk] Microsoft.AspNetCore.App 10.0.13 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 10.0.13 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]nginx listens on ports 80 and 443 on every address. Kestrel (the dotnet process) listens on loopback only:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/pid=[0-9]+,fd=[0-9]+/.../g' | sort -u0.0.0.0:22 users:(("sshd",...),("systemd",...)) 0.0.0.0:443 users:(("nginx",...),("nginx",...),("nginx",...)) 0.0.0.0:80 users:(("nginx",...),("nginx",...),("nginx",...)) 127.0.0.1:5000 users:(("dotnet",...)) 127.0.0.53%lo:53 users:(("systemd-resolve",...)) 127.0.0.54:53 users:(("systemd-resolve",...)) [::]:22 users:(("sshd",...),("systemd",...)) [::]:443 users:(("nginx",...),("nginx",...),("nginx",...)) [::]:80 users:(("nginx",...),("nginx",...),("nginx",...))Your instance's URL and admin password
Read the credentials file:
sudo cat /root/dotnet-credentials.txtOnly root can read the file, which looks like this:
# .NET Web App Host 10.0.113 - generated on first boot by dotnet-firstboot.service
# The admin password is unique to this instance; none was baked into the image. Keep this file secret. ASPNET_APP_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/ ASPNET_ADMIN_USER=admin ASPNET_ADMIN_PASSWORD=<ASPNET_ADMIN_PASSWORD> ASPNET_TLS_CERT=/etc/nginx/tls/dotnet.crt ASPNET_CERT_SHA256=<sha256-fingerprint> ASPNET_APP_DIR=/srv/aspnet/app
#
# nginx (HTTPS, HTTP Basic auth) is the only public entry point; the app runs under Kestrel on 127.0.0.1:5000.
# /health is an unauthenticated probe answered by nginx. Your app is deployed with:
sudo aspnet-app-deploy
# New admin password:
sudo dotnet-rotate-password
# Change the name it serves as:
sudo dotnet-set-hostname <your-dns-name>
# User guide: https://secureanycloud.com/stacks/net-web-app-host-10-0-113/The URL uses the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve your own DNS name, see Step 9.
Open the app
From your own computer, open the ASPNET_APP_URL in a browser. The certificate is self-signed, so the first time the browser asks you to confirm it. Then sign in as admin with the password from Step 4.
The ASP.NET Core app renders the landing page on every request. Further down, the page shows where everything lives on the app volume and which endpoints the sample app serves.
The /api/info endpoint returns the same facts as JSON.
On the instance, you can run the same checks with curl. nginx answers /health without a password, and every other path needs one. curl reads the password from the root-only file through its configuration on standard input, so it never appears on a command line:
H=$(sudo sed -n 's#^ASPNET_APP_URL=https://\(.*\)/$#\1#p' /root/dotnet-credentials.txt)
curl -s --cacert /etc/nginx/tls/dotnet.crt --resolve "$H:443:127.0.0.1" "https://$H/health" curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/nginx/tls/dotnet.crt --resolve "$H:443:127.0.0.1" "https://$H/api/info"
sudo sed -n 's/^ASPNET_ADMIN_PASSWORD=\(.*\)/user = "admin:\1"/p' /root/dotnet-credentials.txt | curl -s --cacert /etc/nginx/tls/dotnet.crt --resolve "$H:443:127.0.0.1" -K - "https://$H/api/info"; echook 401 { "product": "sac .NET Web App Host", "runtime": ".NET 10.0.13", "aspNetCoreAssemblyVersion": "10.0.0.0", "clrVersion": "10.0.13", "osDescription": "Ubuntu 24.04.5 LTS", "architecture": "X64", "processorCount": 2, "serverTimeUtc": "2026-10-20T05:36:01Z", "startedUtc": "2026-10-20T05:30:06Z", "uptimeSeconds": 355, "requestsServed": 1, "dataVolume": "/srv/aspnet", "dataVolumeSpace": "18.5 GiB free of 19.5 GiB", "selfUrl": "https://ec2-<public-ip-dashed>.compute-1.amazonaws.com", "requestHost": "ec2-<public-ip-dashed>.compute-1.amazonaws.com", "requestScheme": "https", "clientIp": "127.0.0.1" }Port 80 redirects to HTTPS on the configured name. A host name that the instance does not serve gets no answer at all: nginx closes the connection, which curl shows as 000.
H=$(sudo sed -n 's#^ASPNET_APP_URL=https://\(.*\)/$#\1#p' /root/dotnet-credentials.txt)
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/api/info"
curl -sk -o /dev/null -w '%{http_code}\n' --resolve "other.example:443:127.0.0.1" https://other.example/ || true301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/api/info 000Check the security posture
The shipped checker proves the whole front door with this instance's own password. It verifies TLS, the redirect, host pinning, authentication and the configured public URL. It also confirms that Kestrel refuses connections on the private address while nginx answers on it:
sudo /usr/local/sbin/dotnet-credential-round-trip.shround-trip: ok 80 -> 301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/api/info round-trip: ok verified TLS /health (no auth) -> 200 round-trip: ok unknown Host on 443 dropped -> 000 round-trip: ok anonymous / -> 401 round-trip: ok anonymous /api/info -> 401 round-trip: ok wrong password -> 401 round-trip: ok instance password / -> 200 round-trip: ok app runs the pinned .NET 10.0.13 round-trip: ok selfUrl is the configured https://ec2-<public-ip-dashed>.compute-1.amazonaws.com round-trip: ok absolute redirect -> https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/ round-trip: ok Kestrel listens on 127.0.0.1:5000 only round-trip: ok <private-ip>:5000 refused round-trip: ok positive control https://<private-ip>/health -> 200 round-trip: RESULT OKThe app runs as aspnet. The app, nginx and the first boot unit are all denied access to the instance metadata service (169.254.169.254), so a server-side request forgery in your app cannot reach instance credentials:
ps -o user=,args= -C dotnet systemctl show -p IPAddressDeny aspnet-app nginx dotnet-firstbootaspnet /usr/bin/dotnet /srv/aspnet/app/SacAppHost.dll IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32Build and deploy your own app
The SDK on the image builds and publishes apps as any user. Create an app in your home directory. The web template needs no packages from nuget.org.
Give the app a page that shows what reaches it through nginx, then publish it:
mkdir -p ~/apps && cd ~/apps && dotnet new web -o HelloApp --no-update-check cat > ~/apps/HelloApp/Program.cs <<'CS' using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); // nginx on 127.0.0.1 sets X-Forwarded-For (the real client) and X-Forwarded-Proto (https). builder.Services.Configure<ForwardedHeadersOptions>(o => o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto); var app = builder.Build(); app.UseForwardedHeaders(); app.MapGet("/", (HttpContext ctx) => $"Hello from my own ASP.NET Core app: scheme {ctx.Request.Scheme}, client {ctx.Connection.RemoteIpAddress}\n"); app.Run(); CS cd ~/apps/HelloApp && dotnet publish -c Release -o out --nologoThe template "ASP.NET Core Empty" was created successfully. Processing post-creation actions... Restoring /home/ubuntu/apps/HelloApp/HelloApp.csproj: Determining projects to restore... Restored /home/ubuntu/apps/HelloApp/HelloApp.csproj (in 146 ms). Restore succeeded. Determining projects to restore... All projects are up-to-date for restore. HelloApp -> /home/ubuntu/apps/HelloApp/bin/Release/net10.0/HelloApp.dll HelloApp -> /home/ubuntu/apps/HelloApp/out/aspnet-app-deploy copies the published output into /srv/aspnet/app, points aspnet-app.service at your entry assembly and restarts it.
Your app inherits the unit's loopback bind (ASPNETCORE_URLS=http://127.0.0.1:5000), its sandbox and the metadata block. nginx keeps HTTPS and the admin password in front of it:
sudo aspnet-app-deploy ~/apps/HelloApp/out HelloApp.dll
H=$(sudo sed -n 's#^ASPNET_APP_URL=https://\(.*\)/$#\1#p' /root/dotnet-credentials.txt)
sudo sed -n 's/^ASPNET_ADMIN_PASSWORD=\(.*\)/user = "admin:\1"/p' /root/dotnet-credentials.txt | curl -s --cacert /etc/nginx/tls/dotnet.crt --resolve "$H:443:127.0.0.1" -K - "https://$H/"aspnet-app-deploy: HelloApp.dll is running behind nginx on 127.0.0.1:5000 Hello from my own ASP.NET Core app: scheme https, client 127.0.0.1In a browser, the same page now answers at your instance's URL.
Keep these points in mind when you deploy your own app:
- Publish framework-dependent, which is the default. The app then runs on the shared runtime from Ubuntu, which unattended upgrades keep patched.
aspnet-app-deployrefuses a self-contained publish. - The app can write only to
/srv/aspnet/keys(the Data Protection key ring) and/srv/aspnet/logs. The rest of the file system is read-only to it. To give it another writable directory, add aReadWritePaths=line withsudo systemctl edit aspnet-app. - Use
ForwardedHeadersas shown above, so that your app sees the real client address andhttps. nginx always sends the configured host name, and the app'sAllowedHostsis set to the instance's names in/etc/aspnet-app/app.env. - To serve the app without the admin password, for example as a public website, remove the two directives from
/etc/nginx/dotnet-auth.confand runsudo systemctl reload nginx. HTTPS, host pinning and the client address handling stay in place.
To go back to the sample app, publish it from its source on the app volume:
cp -r /srv/aspnet/src/SacAppHost ~/apps/ && cd ~/apps/SacAppHost && dotnet publish -c Release -o out --nologo sudo aspnet-app-deploy ~/apps/SacAppHost/out SacAppHost.dllDetermining projects to restore... Restored /home/ubuntu/apps/SacAppHost/SacAppHost.csproj (in 108 ms). SacAppHost -> /home/ubuntu/apps/SacAppHost/bin/Release/net10.0/SacAppHost.dll SacAppHost -> /home/ubuntu/apps/SacAppHost/out/ aspnet-app-deploy: SacAppHost.dll is running behind nginx on 127.0.0.1:5000Replace the admin password
Generate a new admin password, then confirm the permissions on the credentials file and the nginx password file:
sudo dotnet-rotate-password
sudo stat -c '%a %U:%G %n' /root/dotnet-credentials.txt /etc/nginx/.aspnet_htpasswd
sudo cut -c1-12 /etc/nginx/.aspnet_htpasswddotnet-rotate-password: new admin password written (see /root/dotnet-credentials.txt); nginx reloaded 600 root:root /root/dotnet-credentials.txt 640 root:www-data /etc/nginx/.aspnet_htpasswd admin:$2y$12The new password is in /root/dotnet-credentials.txt, and nginx stores only its bcrypt hash. The old password stops working at once.
Your own DNS name and certificate
Point a DNS record at the instance. An Elastic IP keeps the address stable. Then tell the instance its name. This command mints a new self-signed certificate for the name, pins nginx to it, and updates the app's public URL and AllowedHosts:
sudo dotnet-set-hostname app.example.comFor a publicly trusted certificate, you have two options. Put an Application Load Balancer with an AWS Certificate Manager certificate in front of the instance, with target port 443 and health check path /health. Or replace /etc/nginx/tls/dotnet.crt and dotnet.key with your own certificate and key, and reload nginx.
Logs
View the app log and the nginx access log:
journalctl -u aspnet-app -n 4 --no-pager -o cat
sudo tail -n 2 /var/log/nginx/access.log | cut -c1-120info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production info: Microsoft.Hosting.Lifetime[0] Content root path: /srv/aspnet/app 127.0.0.1 - admin [20/Oct/2026:05:37:34 +0000] "GET / HTTP/1.1" 200 78 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15 127.0.0.1 - admin [20/Oct/2026:05:37:48 +0000] "GET /api/info HTTP/1.1" 200 637 "-" "curl/8.5.0"nginx logs the real client address taken from the connection. A client cannot change it with an X-Forwarded-For header.
Server components
The image places these components at the following locations:
- .NET SDK 10.0.113, ASP.NET Core and .NET runtimes 10.0.13: /usr/lib/dotnet (Ubuntu packages dotnet-sdk-10.0, aspnetcore-runtime-10.0)
- Published app, run by aspnet-app.service as aspnet: /srv/aspnet/app
- Sample source: /srv/aspnet/src/SacAppHost
- Data Protection key ring and logs: /srv/aspnet/keys, /srv/aspnet/logs
- App configuration (public URL, AllowedHosts, entry assembly): /etc/aspnet-app/app.env
- nginx site, auth and host includes: /etc/nginx/sites-available/sac-aspnet, /etc/nginx/dotnet-auth.conf, /etc/nginx/dotnet-host.conf
- Certificate and key: /etc/nginx/tls/dotnet.crt, /etc/nginx/tls/dotnet.key
- Build provenance (package versions and sha256): /opt/sac/dotnet-versions.txt, /opt/sac/dotnet-licence-provenance.txt
The .NET packages come from the Ubuntu archive and are MIT-licensed. The licence ships with the binaries:
head -3 /usr/lib/dotnet/LICENSE.txt grep -E '^(dotnet-sdk-10.0|aspnetcore-runtime-10.0|PINNED)' /opt/sac/dotnet-versions.txt | cut -c1-120 grep TELEMETRY /etc/environmentThe MIT License (MIT) Copyright (c) .NET Foundation and Contributors aspnetcore-runtime-10.0 10.0.13-0ubuntu1~24.04.1 sha256=33f942d6444eafe4b4e1e75d856acb00c10ed32e77d1db76efe62e36b20a11c0 dotnet-sdk-10.0 10.0.113-0ubuntu1~24.04.1 sha256=af10918d841dacf97dc7d599536723ad3cac1879dd46b5fa3f081c021cbe967b pool=p PINNED_SDK=10.0.113 PINNED_ASPNETCORE_RUNTIME=10.0.13 PINNED_NETCORE_RUNTIME=10.0.13 DOTNET_CLI_TELEMETRY_OPTOUT=1Updating .NET
.NET security releases for Ubuntu 24.04 arrive through noble-security, and unattended upgrades are switched on. The SDK and runtimes are therefore patched like any other package:
apt-cache policy dotnet-sdk-10.0 | head -4 systemctl is-enabled unattended-upgradesdotnet-sdk-10.0: Installed: 10.0.113-0ubuntu1~24.04.1 Candidate: 10.0.113-0ubuntu1~24.04.1 Version table: enabledAfter a runtime update, restart your app so that it loads the new runtime with sudo systemctl restart aspnet-app.
To update by hand, run sudo apt-get update && sudo apt-get install --only-upgrade dotnet-sdk-10.0 aspnetcore-runtime-10.0.
Support
SecureAnyCloud provides 24/7 technical support for this image by email and live chat at sales@secureanycloud.com.
See the app stacks and runtimes listing guide for the other versions in this listing.
Deploy on AWS
Launch .NET Web App Host 10.0.113 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com