.NET 10 Web App host
Overview
This image is a ready-to-run host for ASP.NET Core web applications. It is not a bare runtime. The .NET SDK and the matching shared runtime are installed and pinned, a real ASP.NET Core application is already compiled and serving under Kestrel, and nginx sits in front of it as the single public entry point.
Everything you own lives on a dedicated data volume mounted at /srv/aspnet, separate from the operating system disk. That includes the published application, its source, the Data Protection key ring and its logs.
Where the .NET binaries come from, and why it matters. .NET is open source under the MIT license, but Microsoft's own Windows builds ship under the Microsoft .NET Library License, whose distribution terms are not compatible with redistributing a runtime as a standalone image. This image sidesteps that question. It installs .NET only from the Ubuntu archive (dotnet-sdk-10.0 and its runtimes, from noble-updates/main and noble-security/main), which Canonical builds from the MIT-licensed source.
Those packages are in Ubuntu's main component, so they carry Canonical's license review and receive security updates through the normal apt channel. Nothing from packages.microsoft.com is installed, and an automated build gate refuses to proceed if that ever changes.
Read this before you expose the VM. The application is bound to 127.0.0.1:5000 and cannot be reached from the network directly. nginx on port 80 is the only public listener, and it requires HTTP Basic authentication on every path except /health, using a password generated uniquely on the first boot of every VM.
Neither nginx nor the application starts until that first boot completes. There is no window in which the machine is reachable without a credential, and no default password exists in the image.
What you get:
- .NET SDK 10.0.110 and the .NET / ASP.NET Core 10.0.10 runtimes, from the Ubuntu 24.04 archive (
main), pinned and checksum-verified at build time. - .NET 10 is an LTS release, supported until November 2028.
- A working ASP.NET Core application, compiled on the image, running under Kestrel bound to
127.0.0.1:5000only. - A dedicated
aspnetservice account withNoNewPrivileges,ProtectSystem,ProtectHomeand a restricted writable path set. - nginx on port 80 as the only public listener, enforcing HTTP Basic authentication on every path.
- A per-VM password, generated on first boot and recorded in a root-only file.
- A per-VM ASP.NET Core Data Protection key ring, created on first boot and never baked into the image.
- A dedicated 20 GiB data volume at
/srv/aspnetfor your application, its source and its keys. - An unauthenticated
/healthendpoint for Azure Load Balancer health probes. - 24/7 SecureAnyCloud support.
.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 active Azure subscription, an SSH key pair, and a VNet and subnet in the target region.
- Standard_B2s (2 vCPU / 4 GiB RAM) is a reasonable starting point. Size up for heavier applications.
- NSG inbound: allow
22/tcpfrom your management network and80/tcpfor the application.
The image serves plain HTTP on port 80. For production, terminate TLS in front of it with your own domain.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for SecureAnyCloud .NET Web App Host, and select Create.
On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80).
Then select Review + create and Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group my-resource-group \
--name my-dotnet-host \
--image sac1647283583153:dotnet:default:latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard
az vm open-port --resource-group my-resource-group --name my-dotnet-host --port 80Accept the image terms once per subscription, before the first deployment.
az vm image terms accept --publisher sac --offer dotnet --plan defaultStep 3 - Retrieve the per-VM password
Every VM generates its own password on first boot. SSH in and read it with this command.
sudo cat /root/dotnet-credentials.txtThe file shows the application URL, the username admin, and the password unique to this machine. The file is 0600 root:root, so only root can read it.
Confirm the permissions and check that first boot completed.
sudo stat -c '%a %U:%G %n' /root/dotnet-credentials.txt ls -l /var/lib/sac/data/Step 4 - Verify the deployment
Check that the three units are running and that the listeners are where they should be.
systemctl is-active aspnet-app nginx dotnet-firstboot ss -ltnp | grep -E ':(80|5000)'nginx listens on 0.0.0.0:80. The application (dotnet) listens on 127.0.0.1:5000 only. That is the point of the architecture, and Step 7 proves it.
The unauthenticated health probe answers immediately.
curl -s http://127.0.0.1/healthStep 5 - Open the application in a browser
Browse to http://<your-vm-public-ip>/. Your browser prompts for a username and password. Use admin and the password from Step 3.
The ASP.NET Core application renders the page on the server, on that request. The three values at the top are computed live. Reloading the page advances the request counter and moves the clock, which is the simplest proof that the runtime is executing your code rather than serving a static file.
Scroll down to see the request path the response travelled, including the exact Host header nginx forwarded and the loopback address Kestrel is bound to.
Below that, the page lists the storage locations on the dedicated data volume and the endpoints the sample application exposes.
Step 6 - Query the JSON API
The same facts are available as JSON. Use them for monitoring and to confirm what the machine is running.
curl -s -u "admin:<ASPNET_ADMIN_PASSWORD>" http://127.0.0.1/api/infoNote the selfUrl field. It is built from the Host header of your request, so it always reflects the address you used to reach the machine. This includes a non-standard port if you are behind a proxy or a tunnel.
nginx forwards $http_host rather than $host so that this stays correct. $host silently discards the port, which would make every absolute URL the application emits unreachable for anyone not on port 80.
Step 7 - Confirm the security posture
Verify this yourself rather than take it on trust. First, confirm that the server refuses an unauthenticated request.
curl -s -o /dev/null -w 'anonymous request -> HTTP %{http_code}\n' http://127.0.0.1/You should see 401. Now prove that the application port is not exposed. Connect to the Kestrel port on this VM's own routable address.
curl -s -m 5 "http://$(hostname -I | awk '{print $1}'):5000/" ; echo "curl exit=$? (7 = connection refused)"That refusal only means something if the address itself is live, so run a control check against the same address on nginx's port.
curl -s -o /dev/null -w 'same address, port 80 -> HTTP %{http_code}\n' "http://$(hostname -I | awk '{print $1}')/"401 from port 80 and a refused connection on port 5000, from the same address, together prove the application is reachable only through the authenticating proxy.
Finally, confirm that the services are gated on the first-boot marker. This guarantees that no listener exists before a password does.
grep -h ConditionPathExists /etc/systemd/system/aspnet-app.service /etc/systemd/system/nginx.service.d/10-sac-dotnet.confStep 8 - Build and deploy your own application
The SDK is installed, so you can build on the machine you deploy to. Scaffold a new ASP.NET Core application, publish it to the data volume, and run it on a spare port to check that it works.
export DOTNET_NOLOGO=1 DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_GENERATE_ASPNET_CERTIFICATE=false
sudo mkdir -p /srv/aspnet/myapp && cd /srv/aspnet/myapp
sudo -E dotnet new web -o src --force sudo -E dotnet publish src -c Release -o /srv/aspnet/myapp/out --nologoStart it briefly on port 5001 and confirm that it answers.
sudo -E sh -c 'ASPNETCORE_URLS=http://127.0.0.1:5001 dotnet /srv/aspnet/myapp/out/src.dll >/tmp/myapp.log 2>&1 &' sleep 6 curl -s http://127.0.0.1:5001/ ; echo
sudo pkill -f '/srv/aspnet/myapp/out/src.dll' || trueWhen you are ready to make your application the one served on port 80, publish it over /srv/aspnet/app and restart the service. The unit runs SacAppHost.dll, so either name your assembly to match or edit ExecStart.
sudo dotnet publish /srv/aspnet/myapp/src -c Release -o /srv/aspnet/app
sudo sed -i 's|/srv/aspnet/app/SacAppHost.dll|/srv/aspnet/app/src.dll|' \
/etc/systemd/system/aspnet-app.service
sudo systemctl daemon-reload && sudo systemctl restart aspnet-appKeep your application bound to 127.0.0.1. The unit sets ASPNETCORE_URLS=http://127.0.0.1:5000 and nginx proxies to it. Binding to 0.0.0.0 would expose the application past the authenticating proxy.
Step 9 - Check the versions and the licensing
The exact versions the image ships are recorded on disk and reported by the CLI.
dotnet --list-sdks dotnet --list-runtimes cat /opt/sac/dotnet-versions.txtConfirm where the packages came from, and read the license that ships with the binaries.
apt-cache policy dotnet-sdk-10.0 head -3 /usr/lib/dotnet/LICENSE.txtThe full third-party attribution is at /usr/lib/dotnet/ThirdPartyNotices.txt, and the Debian copyright record for every .NET package is under /usr/share/doc/dotnet-*/copyright. A provenance record captured at build time, including the SHA-256 of every package installed, is at /opt/sac/dotnet-licence-provenance.txt.
Step 10 - Change the password
You can replace the first-boot password at any time. Set a new password for the admin user.
sudo htpasswd -b /etc/nginx/.aspnet_htpasswd admin 'your-new-password-here'
sudo systemctl reload nginxThen update /root/dotnet-credentials.txt so the machine's record matches what you set.
Maintenance
Your data volume. /srv/aspnet is a separate 20 GiB managed disk. It holds app/ (the running application), src/ (the sample source), keys/ (the Data Protection key ring, which protects cookies and antiforgery tokens) and logs/. Snapshot this disk to back up your application and its keys.
df -h /srv/aspnet findmnt -no SOURCE,TARGET,FSTYPE,SIZE /srv/aspnetDo not delete /srv/aspnet/keys on a running system. ASP.NET Core uses it to decrypt cookies and antiforgery tokens issued earlier, and removing it signs every user out.
Logs. The application logs to the journal.
sudo journalctl -u aspnet-app -n 30 --no-pagerSecurity updates. Unattended upgrades are enabled, and .NET is updated through the normal Ubuntu security channel because it comes from the archive rather than a vendor feed.
apt-cache policy dotnet-runtime-10.0TLS. The image serves plain HTTP. For production, put your own certificate in front. Either terminate TLS on an Azure Application Gateway or Front Door in front of this VM, or install a certificate directly in the nginx server block at /etc/nginx/sites-available/sac-aspnet.
Resizing. The stack is nginx plus a single .NET process and uses well under 300 MiB at rest, so Standard_B2s is comfortable for the sample. Size up when your own application needs it.
Support
SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com with your Azure subscription ID and the VM name.
.NET and ASP.NET are trademarks of the Microsoft group of companies. SecureAnyCloud is not affiliated with, endorsed by, or sponsored by Microsoft.
Deploy on Azure
Launch SecureAnyCloud .NET Web App Host on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com