Deployment guide

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 /health probe needs HTTP Basic authentication as admin. 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 overwrites X-Forwarded-For and X-Real-IP with the real client address. The sample app trusts forwarded headers from 127.0.0.1 only.
  • 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 Host header can therefore never point absolute links and redirects elsewhere.
  • Sandboxed and fail-closed. The app runs as the unprivileged aspnet user 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/aspnet is 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 main component, so apt and 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 the dotnet command 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.service and nginx.service units, and the helpers aspnet-app-deploy, dotnet-rotate-password and dotnet-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.

1

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}]'
2

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>
3

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-runtimes
active 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 -u
0.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",...))
4

Your instance's URL and admin password

Read the credentials file:

sudo cat /root/dotnet-credentials.txt

Only 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.

5

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"; echo
ok 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/ || true
301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/api/info 000
6

Check 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.sh
round-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 OK

The 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-firstboot
aspnet /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/32
7

Build 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 --nologo
The 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.1

In 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-deploy refuses 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 a ReadWritePaths= line with sudo systemctl edit aspnet-app.
  • Use ForwardedHeaders as shown above, so that your app sees the real client address and https. nginx always sends the configured host name, and the app's AllowedHosts is 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.conf and run sudo 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.dll
Determining 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:5000
8

Replace 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_htpasswd
dotnet-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$12

The new password is in /root/dotnet-credentials.txt, and nginx stores only its bcrypt hash. The old password stops working at once.

9

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.com

For 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.

10

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-120
info: 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.

11

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/environment
The 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=1
12

Updating .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-upgrades
dotnet-sdk-10.0: Installed: 10.0.113-0ubuntu1~24.04.1 Candidate: 10.0.113-0ubuntu1~24.04.1 Version table: enabled

After 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

Deploy on AWS