HTTPS and Transport Security
By the end of this lesson
Protect traffic in transit and enforce it.
A request from an employee's browser to the orders API passes through equipment nobody on your team owns: a wireless access point, an office switch, an internet provider, several networks in between, a load balancer. Over plain HTTP, every one of those sees the request as text.
Seeing it is the smaller half. Anything on the path can also change it — swap a delivery address, alter a total, add a script tag to the response, redirect the login form somewhere else. There is nothing in HTTP that lets either end notice. That is why transport security is not only about confidentiality.
TLS is the protocol that fixes all three parts of the problem at once. HTTPS is HTTP carried over TLS; the terms get used interchangeably and it does no harm. What it gives you is worth stating precisely, because each property rules out a different failure.
The pieces, and what each one is responsible for:
- TLS
- Transport Layer Security. It provides three things: confidentiality, so the path cannot read the traffic; integrity, so the path cannot change it without both ends noticing; and server authentication, so the client knows it is talking to the host it asked for rather than to whatever answered. Remove any one of the three and the other two stop being useful.
- Certificate
- A file the server presents at the start of a connection, containing its public key and the host names it is valid for, signed by an authority the client already trusts. It is how the server proves it is the host in the address bar. Certificates have an expiry date, and that date is an operational concern rather than a detail — see the steps below.
- Certificate authority
- An organisation whose signature browsers already trust, so a certificate it signed is accepted without you configuring anything on the client. Public authorities issue certificates for public host names, usually free and automated. A private internal authority issues certificates for internal host names, which is how services inside your own network get TLS without being publicly resolvable.
- HSTS
- Strict-Transport-Security, a response header telling the browser to use HTTPS for this host for a stated period, and to refuse to continue if the certificate does not check out. It converts your redirect from a suggestion into a rule the browser enforces before any request leaves it.
- TLS termination
- The point where the encrypted connection is decrypted. Often a load balancer or ingress controller rather than your application, which changes two things: your application receives plain HTTP and needs telling that the original request was secure, and the traffic beyond the termination point is only as protected as you have made it.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
// Preload is a one-way door. Browsers ship the list, and removal is slow.
// Turn it on once you are certain every subdomain can serve HTTPS.
options.Preload = false;
});
builder.Services.AddHttpsRedirection(options =>
{
// 308 keeps the method and body on redirect, and is cacheable.
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
});
// Calls between your own services are still worth encrypting. A private
// network is a boundary you share with everything else inside it.
builder.Services.AddHttpClient("pricing", client =>
{
client.BaseAddress = new Uri("https://pricing.internal.example-company.com");
});
var app = builder.Build();
// Behind a load balancer that terminates TLS, the app sees plain HTTP unless
// the forwarded headers are read. Without this, the redirect below can loop.
app.UseForwardedHeaders();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();- UseHttpsRedirection answers a plain HTTP request with a redirect to the HTTPS address. It is the right default and it is not a protection on its own: the first request already travelled in the clear, and a client that ignores redirects is unaffected. Treat it as good manners for humans who typed the address, and let HSTS do the enforcing.
- A max-age of a year is a common choice and it is a commitment. For that period, browsers that have seen the header will not connect to the host over HTTP at all, and will not let the user click past a certificate error. Start with a short max-age on a host you are still changing, and raise it once HTTPS is settled everywhere including every subdomain.
- IncludeSubDomains applies the rule to every subdomain. This is the setting that breaks things, because it catches the internal tool on a subdomain that still runs HTTP. Enumerate your subdomains before switching it on.
- UseHsts is skipped in development deliberately. A browser that has cached the policy for localhost applies it to every other local project on that host, for the whole max-age, which is an unpleasant afternoon.
- 308 rather than the default 307 makes the redirect permanent and cacheable, so repeat visitors skip the extra round trip. Both preserve the method and body; a 301 or 302 historically did not, which is why a redirected POST could arrive as a GET with nothing in it.
- UseForwardedHeaders has to run before anything that looks at the scheme, and it needs configuring with the proxies or networks you trust — by default the middleware ignores headers from unknown sources, which is correct, because those headers are client-supplied. The classic symptom of getting this wrong is an endless redirect loop: the proxy sends HTTPS to your app as HTTP, your app redirects to HTTPS, the proxy terminates it again, and round it goes.
- The named HttpClient points at an internal host over HTTPS. That is a decision, not a default, and the next section is about whether it is worth the cost.
Where TLS stops is a design decision. Terminating at the edge is the common default; carrying it through to each service costs more and assumes less:
| Terminate at the edge only | TLS all the way to each service | |
|---|---|---|
| What is encrypted | Traffic from the client to the load balancer | Every hop, including service to service and service to database |
| Who can read the traffic | Anything inside the network past the termination point | Only the two ends of each connection |
| Certificates to manage | One, at the edge | One per service, usually from an internal authority, issued and rotated automatically |
| Assumption it relies on | The internal network is trustworthy, and everything on it is friendly | None. Each connection proves itself |
| Cost | Lowest. One thing to configure and renew | More setup, more to automate, a little more CPU, and packet captures stop being readable |
| Reasonable when | A small deployment where you control everything past the edge and can say who else is on that network | Shared clusters, several teams, regulated data, or anywhere the network is not exclusively yours |
Certificate expiry takes more services down than attacks do, and it is entirely preventable. What to put in place:
Automate issue and renewal
Use the automated protocol your authority supports, or your platform's managed certificates, so renewal happens without a person. A calendar reminder is not automation: it depends on somebody being available, not on leave, and still working here. Most expiry incidents are a reminder that went to one inbox.
Renew early, not at the deadline
Trigger renewal at roughly two thirds of the certificate's lifetime. That leaves weeks for a failed renewal to be noticed and retried, instead of hours. Renewal fails for ordinary reasons — a DNS change, an expired account, a rate limit — and none of them are urgent if there is a month of headroom.
Monitor the certificate the server actually serves
Check it from outside, over a real connection, on every host and port that terminates TLS. Checking the file on disk misses the two most common failures: a renewed file that the process never reloaded, and a host that is serving a different certificate from the one you think.
Alert on days remaining, to a rota
Warn at thirty days, escalate at seven, page at two. Send it to whoever is on call rather than to a person, and make sure the alert fires on the certificate being close to expiry rather than on the renewal job having failed — the job you forgot to schedule cannot fail.
Write down every place a certificate lives
The edge, each internal service, client certificates for partner integrations, code signing, push notifications, the identity provider's signing certificate. The one that expires is always the one nobody listed. This inventory is dull to make and it is the whole value of this step.
Rehearse a renewal
Renew one certificate deliberately, out of hours, and watch what happens: does the process pick up the new file, does it need a reload, does anything cache the old one. Finding out during a rehearsal is inexpensive. Finding out at the moment of expiry is not.
Summary
- TLS gives confidentiality, integrity and server authentication; without it the path can both read and change traffic
- Redirecting HTTP is a convenience, and HSTS is the enforcement, because the browser applies it before any request leaves
- HSTS cannot cover the first visit on a new device, which is what preloading is for, and preloading is hard to undo
- Certificate expiry causes more outages than attacks, so automate renewal, monitor the served certificate and alert early
- Internal traffic is still worth encrypting, because a network perimeter is not a trust boundary you should rely on
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Is internal traffic worth encrypting?
Your orders API and pricing service run as containers in the same cluster. TLS terminates at the ingress controller, so traffic between the two services is plain HTTP.
A colleague argues that adding TLS between them is effort with no benefit, because the network is private. Give the strongest version of their case, then say what you would do and what it costs.
Show solution
Their case, put fairly: the traffic never leaves the cluster, access to the cluster network is already restricted, there is one more certificate per service to issue and rotate, a small CPU cost, and debugging gets harder because a packet capture is no longer readable. For a single small deployment run by one team, that is a coherent position and plenty of teams hold it.
What it assumes is the weak point. It treats the cluster network as a trust boundary, which means every workload on that network is trusted — including the one that runs a compromised dependency next month, and whatever the platform team schedules onto the same nodes. A boundary that admits anything inside it is not doing the work the argument gives it credit for.
What I would do: encrypt between services, with certificates from an internal authority, issued and rotated automatically. A service mesh or your platform's built-in support can do this without application changes, which is what makes the cost acceptable — the objection is mostly about certificate management, and automation is the answer to certificate management.
The cost is real and worth stating rather than waving away: more moving parts, another thing that can fail at renewal, and a debugging step where you have to work at the application's logs instead of the wire. The reason to pay it is that the alternative's safety depends on a claim about the network that you cannot keep making as the cluster grows.
There is a middle position that is defensible: encrypt anything crossing a node, a cluster or a network boundary, and anything carrying credentials or personal data, and accept plain HTTP for a health check. Deciding that deliberately is different from leaving it plain because nobody asked.
Try it yourself
Diagnose the redirect loop
You deploy the orders API behind a load balancer that terminates TLS and forwards requests to the container as HTTP on port 8080. Browsers now report too many redirects. The application worked when it ran locally over HTTPS.
Explain what is happening, and what you would change. Then say why the fix needs configuration rather than only a line of code.
Show solution
The application sees a plain HTTP request, because the load balancer decrypted the connection and forwarded HTTP. UseHttpsRedirection does what it is told and answers with a redirect to HTTPS. The browser follows it, the load balancer terminates TLS again and forwards HTTP again, and the application redirects again. Neither part is broken; they disagree about what the original request was.
The load balancer already states the truth in the X-Forwarded-Proto header. The fix is to read it, with UseForwardedHeaders placed before anything that inspects the scheme, so the application treats the request as secure and stops redirecting.
The configuration matters because those headers come from the client's side of the connection. Anything can send X-Forwarded-Proto. If the middleware accepted it unconditionally, a request could claim to be secure when it was not, which would defeat the check you added the middleware to support. So you list the proxies or networks whose headers you trust, and the middleware ignores the rest — which is also why it appears to do nothing until it is configured, and why that confuses people the first time.
Worth checking at the same time: that the load balancer refuses plain HTTP on the outside, or redirects it itself. Once the application trusts forwarded headers, its own redirect only fires for requests the proxy reports as insecure, so the outermost hop is where HTTP has to be dealt with.
Saved in this browser only.