Start with the basics
Most Umbraco sites that get compromised weren't hit by a clever zero-day. They were running an out-of-date version, had a weak backoffice password, or had a secret committed to Git. Fix those first.
- Stay on a supported version and apply patches quickly. Watch Umbraco's security advisories, or let us do it with a security retainer.
- Scan your packages with the Umbraco Security Dashboard.
- Remove packages and features you don't use, including the Delivery API if you don't need it.
Step 1: HTTPS everywhere
{
"Umbraco": {
"CMS": {
"Global": { "UseHttps": true }
}
}
}And in Program.cs, redirect HTTP and send HSTS in production:
if (app.Environment.IsDevelopment() == false)
{
app.UseHsts();
}
app.UseHttpsRedirection();Step 2: Security headers
Add a small middleware. It sets a strict Content Security Policy on the public site but leaves the backoffice alone, because the backoffice needs a looser policy:
namespace MySite.Security;
public static class SecurityHeadersExtensions
{
/// <summary>Adds common security headers to every response. Register before app.UseUmbraco().</summary>
public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder app) =>
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
var headers = context.Response.Headers;
headers["X-Content-Type-Options"] = "nosniff";
headers["X-Frame-Options"] = "SAMEORIGIN";
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
// The backoffice needs a looser policy, so only apply the CSP to the public site.
if (context.Request.Path.StartsWithSegments("/umbraco") == false)
{
headers["Content-Security-Policy"] =
"default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; script-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'";
}
return Task.CompletedTask;
});
await next();
});
}Register it in Program.cs before app.UseUmbraco():
app.UseSecurityHeaders();Adjust the CSP to match your site, for example to allow your analytics or CDN. Check the result with securityheaders.com.
Step 3: Lock down the backoffice
- Restrict
/umbracoby IP address where you can: in nginx withallow/deny, in IIS with IP Address and Domain Restrictions, or with a WAF rule. Allow your office, VPN or Azure Front Door only. - Use single sign-on (Microsoft Entra ID, Google) through Umbraco's external login providers, so your identity provider's multi-factor sign-in protects the backoffice. Umbraco also supports two-factor authentication for users once you register a 2FA provider.
- Give users only the access they need. Create user groups for editors, and keep Administrators small.
- Remove old accounts when people leave.
Step 4: Strong passwords and lockout
{
"Umbraco": {
"CMS": {
"Security": {
"UserPassword": {
"RequiredLength": 14,
"MaxFailedAccessAttemptsBeforeLockout": 5
},
"MemberPassword": {
"RequiredLength": 12,
"MaxFailedAccessAttemptsBeforeLockout": 5
}
}
}
}
}Step 5: Keep secrets out of Git
Connection strings, SMTP passwords, API keys and webhook secrets belong in environment variables, user secrets or Azure Key Vault, not in appsettings.json. If a secret was ever committed, rotate it: deleting it from the repository isn't enough, because it stays in the Git history.
Step 6: Check regularly
- Run Settings › Health Check in the backoffice. It flags insecure settings such as debug mode or missing HTTPS.
- Make sure
Umbraco:CMS:Hosting:Debugis false in production. - Book a penetration test once a year, or after big changes.
Need a hand?
umbraco.ninja has more than 20 years of Umbraco experience. If you'd rather not do this yourself, we'll set it up, test it and hand it over.
Get a fixed-price quote