Secure Netlify with cnspec
Scan Netlify accounts and sites against security and compliance best practices with cnspec.
Scan your Netlify accounts and sites to find security risks before they become incidents. cnspec evaluates who can administer an account, multi-factor and SAML enforcement, what a build is allowed to do, which environment variables are exposed to runtime code, what markup is injected into served pages, DNS zone contents, deploy key hygiene, and dozens of other Netlify controls.
If you're new to cnspec, start with the Quickstart. For an overview of every SaaS service cnspec can scan, see the SaaS scanning overview.
Prerequisites
To scan Netlify with cnspec, you must have:
- cnspec installed on your workstation
- A Netlify account with access to the sites you want to scan
- A Netlify personal access token
Authenticate
cnspec connects to Netlify using a personal access token. To create one:
- Log in to the Netlify app.
- Go to User settings > Applications > Personal access tokens.
- Create a token and copy the value. Netlify shows the token only once.
The token inherits the roles of the account that issued it. A token issued by a member rather than an owner can't read the account roster or some site settings. Those fields come back null instead of failing the scan, so a check never passes on data cnspec was never allowed to read.
You can pass the token on the command line with --token, or export it once and reuse it across commands:
export NETLIFY_AUTH_TOKEN=YOUR_TOKENcnspec also accepts NETLIFY_TOKEN. When either variable is set, you can omit the --token flag from the commands below.
By default cnspec discovers every account the token can access, along with the sites those accounts own. To scope a scan to a single account, pass its slug or ID with --account.
Connection options
| Option | Description |
|---|---|
--token | Netlify personal access token for authentication |
--account | Scope discovery to a single Netlify account (slug or ID) |
--discover | What to discover: auto (default), all, accounts, sites |
Verify with a quick Netlify check
Confirm that cnspec can reach your Netlify account by opening a cnspec shell:
cnspec shell netlify --token YOUR_TOKENcnspec> netlify.currentUser { email fullName mfaEnabled }
netlify.currentUser: {
email: "admin@example.com"
fullName: "Acme Admin"
mfaEnabled: true
}If cnspec connects and shows your account, you're ready to scan. An empty account list means the token was accepted but isn't a member of any account.
Scan Netlify
cnspec scan netlify --token YOUR_TOKENTo scan a single account:
cnspec scan netlify --token YOUR_TOKEN --account acmeWhen a scan completes, cnspec prints a summary of all the checks it ran, grouped by policy, along with a risk score from 0 (no risk) to 100 (highest risk). Failed checks include remediation guidance to help you fix issues. To learn more about reading scan results, read Understand cnspec Results.
Mondoo doesn't yet ship an out-of-the-box Netlify policy, so use the checks below as a starting point and create your own policies to meet your specific requirements.
Explore and test checks interactively
Open a cnspec shell to discover resources and try out checks:
cnspec shell netlify --token YOUR_TOKENList all accounts
cnspec> netlify.accounts { slug typeName enforceMfa samlEnabled }
netlify.accounts: [
0: {
slug: "acme"
typeName: "Enterprise"
enforceMfa: "not_enforced"
samlEnabled: true
}
]List all sites
cnspec> netlify.sites { name url repoUrl forceSsl }List account members and owners
Reading the roster takes administrative rights on the account. Where the token lacks them, members is null rather than empty:
cnspec> netlify.accounts { slug members { email role mfaEnabled } }
cnspec> netlify.accounts { slug owners { email role } }Inspect a site's environment variables
Netlify withholds the value of a variable stored as a secret, but the key, scopes, and per-context values of every other variable are readable:
cnspec> netlify.sites[0].environmentVariables { key isSecret scopes }List the snippets injected into a site
A snippet is markup Netlify serves on every page of the site, and it lives outside the repository, so it escapes code review:
cnspec> netlify.sites { name snippets { title generalPosition general } }List DNS zones and their records
cnspec> netlify.dnsZones { name records { hostname type value managed } }Walk from a resource to its neighbors
Sites, accounts, DNS zones, and deploy keys reference each other, so a query can start from any of them and walk in either direction:
cnspec> netlify.dnsZones { name site { name forceSsl } }
cnspec> netlify.sites { name account { slug typeName } }
cnspec> netlify.sites { name deployKey { id createdAt } }Example security checks
Ensure all accounts enforce multi-factor authentication
enforceMfa reports the account's setting as Netlify names it, so test for the unenforced value rather than assuming the name of the enforced one:
cnspec> netlify.accounts.none(enforceMfa == "not_enforced")
[ok] value: trueEnsure every account member has enrolled a second factor
cnspec> netlify.accounts.all(members.all(mfaEnabled == true))
[ok] value: trueEnsure no invitation stays outstanding
cnspec> netlify.accounts.all(members.none(pending == true))
[ok] value: trueEnsure nobody can join the account without an invitation
Every domain in teamRegistrationDomains lets any address at that domain add itself to the account:
cnspec> netlify.accounts.all(teamRegistrationDomains.length == 0)
[ok] value: trueEnsure SAML sessions expire within a day
cnspec> netlify.accounts.where(samlEnabled == true).all(samlSessionExpiration <= 86400)
[ok] value: trueEnsure Netlify support can't administer the account
cnspec> netlify.accounts.all(supportAdministrationEnabled == false)
[ok] value: trueEnsure every site redirects visitors to HTTPS
cnspec> netlify.sites.all(forceSsl == true)
[ok] value: trueEnsure production can't be published outside of git
cnspec> netlify.sites.all(preventNonGitProdDeploys == true)
[ok] value: trueEnsure builds from untrusted contributors are reviewed
cnspec> netlify.sites.all(untrustedFlow == "review")
[ok] value: trueEnsure no environment variable exposed to runtime code is stored in plaintext
A variable scoped to runtime is readable by the code the site serves, so it belongs in secret storage:
cnspec> netlify.sites.all(environmentVariables.none(isSecret == false && scopes.contains("runtime")))
[ok] value: trueEnsure build logs are restricted to account members
The control is tri-state. A site that has never set it reports null and follows the team default, so test for == true to catch both the explicitly disabled site and the one still inheriting:
cnspec> netlify.sites.all(privateLogs == true)
[ok] value: trueTo isolate just the sites inheriting the team default, filter on null:
cnspec> netlify.sites.where(privateLogs == null) { name repoUrl }Ensure no deploy key grants repository access without a site
A key that no site clones with still authorizes reads of the repository it was added to:
cnspec> netlify.deployKeys.all(sites.length > 0)
[ok] value: trueReview DNS records that delegate a name elsewhere
An unmanaged CNAME points at infrastructure Netlify doesn't control, which is where dangling records come from:
cnspec> netlify.dnsZones { name records.where(type == "CNAME" && managed == false) { hostname value } }Learn more
- Netlify Resource Pack Reference: every Netlify resource and field cnspec can query
- Write Effective MQL: guide to authoring checks and queries