In the last post, we built a simple .Net Core application to handle payment processing for our event registration. It works like a champ on our local machine, but it doesn't do us any good if no one can access it.
Today, we'll make it available to the public.
I mostly started with this helpful article in the Microsoft Docs. However, I found some issues that I had to work around.
A Quick Update to the App
Using Nginx, we'll need to be able to reverse proxy, so we need to add a little bit of code to our project to handle this. In startup.cs
, we'll add to the Configure
method.
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
You may need to add to your using statements.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
Now we can carry on to building out the server.
Setting Up the Server
I started with a Linode "Nanode" server with Ubuntu 18.04 installed. In my case, this page only needs to be available for three months and won't see a ton of traffic, so the small size and smaller cost ($5 a month) really suits what I'm trying to do.
Go through your normal setup (update, upgrade, secure ssh, etc...) and then we'll install the other pieces we need.
Installing Dotnet on the Server
Again, I referenced Microsofts Docs for this, but I condensed the commands together for the sake of brevity.
wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo add-apt-repository universe
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install dotnet-sdk-2.2
Copying Code and Building
Depending on how intense you may want to go with this, you may want to build out a full CI pipeline. In my case, I'm not going to be doing any iterations, so I just copied the code over using scp
and ran it on the server.
scp -r ~/path/to/directory ssh@server:~/appname
That command (once populated with your information) should copy the entire folder from your machine to the server in the SSH users home directory.
SSH into the server and build/publish the application.
dotnet build
dotnet publish --configuration Release
Publish should move everything into a folder in the bin/Release/netcore2.2/
directory.
Once that is done and successful, I then use symbolic linking to put it in /var/www
. It's not necessary to do it, but it's an old habit.
sudo ln -s ~/appname/bin/Release/netcore2.2/publish /var/www/appname
This links the folder in the home directory to the /var/www/
directory, so if you do need to make fixes, there's no worry about copying things around.
Installing and Setting Up CertBot
Certbot is pretty straight forward and it's a beautiful tool I've written a bit about.
First, install through apt
.
sudo apt install -y certbot
Once that is done, we can request a certificate. I prefer to use the standalone
method.
sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com
Once that is finished, you should be able to run sudo certbot certificate
and see the location of the certificate on your machine. It's important to keep this somewhere, we'll need it in the next step.
Install and Configure Nginx
We have code, we have a certificate, now we just need to make it available to the adoring masses.
First, install Nginx:
sudo apt install nginx
Second, make sure you can access it through your firewall. Assuming you're using ufw
, we can just update the permissions.
sudo ufw allow 'Nginx Full'
Now, if you navigate to the IP address of your server, you should see the "welcome to nginx" screen.
Once you've verified that you're able to access the server, you'll want to make sure your domain is pointed to the IP. This is going to vary wildly based on what your DNS provider is, but I believe in you.
With a domain, we can now handle setting up Nginx for our site. I'll save the boring details of how all of this works, but here's what we need to do.
Create a file in /etc/nginx/sites-available/
:
sudo nano /etc/nginx/sites-available/yourdomain.conf
In the text editor, copy in this configuration:
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl on;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
gzip on;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_types text/plain text/html text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml;
gzip_buffers 16 8k;
gzip_disable “MSIE [1-6].(?!.*SV1)”;
access_log /var/log/nginx/yourdomain .access.log;
location / {
proxy_pass https://localhost:5001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Again, I'm being brief here, but this should do everything we need to take requests to yourdomain.com
, forward them to SSL, and connect them to the application running on the server.
In order to enable it, we'll make another symbolic link and then restart Nginx.
sudo ln -s /etc/nginx/sites-available/yourdomain.conf /etc/nginx/sites-enabled/yourdomain.conf
sudo systemctl restart nginx
To make sure everything is working, we'll start the app on its own and try to access it through the browse.
dotnet run /var/www/yourapp/yourapp.dll
After you've verified that this works, you can close the application. The next step is to make it working on its own.
Setting up systemd
Yes, there is some tragedy around systemd
, but it's still handy.
Create a .service
file for you application:
sudo nano /etc/systemd/system/yourapp.service
Add the following information to the file:
[Unit]
Description=Event Registration Example
[Service]
WorkingDirectory=/var/www/yourapp
ExecStart=/usr/bin/dotnet /var/www/yourapp/yourapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
The only thing I changed here from the Microsoft Doc was removing the User=www-data
line. While it is best practice to have a limited scope user, I didn't have a need to worry about this service running as root. This is a non-critical system that handles next to nothing other than a redirect, so I thought it was safe enough.
Now we can enable and start the service through systemd.
sudo systemctl enable yourapp.service
sudo systemctl start yourapp.service
sudo systemctl status yourapp.service
If everything has gone to plan, you should receive a status of running and be able to access your page. If it has all gone pair shape, you can still access the logs.
sudo journalctl -fu yourapp.service
That's It!
Hope this helps. There are a lot of ways this can be optimized or better secured, but for something this simple I didn't sweat it too hard. It's been running for a couple weeks and processed a handful of payments from people we gave early access to.
I did make some subtle styling changes, but that's about it. Works perfect and is relatively easy to modify going forward.
Thanks for coming along on the ride!
Top comments (7)
Thanks so much for this. I've just had a need to do this, and pulling together the mishmash of documentation and working out which bits are wrong has been tiring. Your guide worked for me from start to finish first time.
By the way, you haven't swapped out a placeholder value in the nginx config for 443 server_name.
Good catch!
Also, glad I could help. I found the Microsoft docs to be lacking in a real world setup. So I kind of just pieced it together the rest of the way.
LetsEncrypt have revoked around 3 million certs last night due to a bug that they found. Are you impacted by this, Check out ?
DevTo
[+] dev.to/dineshrathee12/letsencrypt-...
GitHub
[+] github.com/dineshrathee12/Let-s-En...
LetsEncryptCommunity
[+] community.letsencrypt.org/t/letsen...
Hi Ian,
Firstly thanks for this article, it's been a major help, completely agree that the Microsoft docs are not up to scratch.
One issue I've had is that my aspnet core app would only start up as http:// localhost:5000. on my ubuntu server.
I understand that's because kestrel didn't have access to it's self signed cert like it does when running locally.
I've got around this by setting nginx to direct to http:// localhost:5000 and removing app.UseHttpsRedirection(); in startup.cs. I'm assuming this is fine considering nginx is the reverse proxy.
Your article shows that you are directing nginx to https:// localhost:5001 yet you don't mention this issue? Can you elaborate on this please? Would be super helpful.
I had this same issue when revisiting the app I was working on back in July. Now I'm using .NET Core 3.1 instead of 2.2. I ran a command on the Ubuntu server to have dotnet generate a new self-signed cert (I believe it was "dotnet dev-certs https"). This allowed the .NET Core Web API app to run using Https/5001.
I believe you are correct in your thinking with this sort of "jankness" being okay because it's running behind Nginx. When I go to my app in a browser, it is using the LetsEncrypt certificate, so the self-signed cert just seems to be there to check a box that dotnet goes through when starting the app.
Friendly note on the LetsEncrypt... the .NET app cannot be running when you run the certbot command to refresh the cert, else you get an error about not being able to bind to port 80 (has nothing to do with you using 443 only, it's just that it tries 80 before 443 so we see that 80 error first). At first I thought the opposite, that an app needed to be running on 80/443 for the Letsencrypt people to see it's a valid website.
For forcing https/5001 only in the .NET Core Web API app, I added "app.UseHttpsRedirection();" to Startup.cs and "webBuilder.UseUrls("localhost:5001");" to Program.cs. It's okay to "disable" http/80 within the .NET Core Web API app as Nginx redirects 80 to 443 and then sends it to the .NET Core app. I know you knew that, just being detailed for others who may happen by ;)
You could use crontab with @reboot instead of creating a service to start the app automatically.
Thanks for helpful post, and I need to ask you in my case I use IdentityServer4 which only run on https so how can I install the same certificate on kestrel as well?