SaaS

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:

Authenticate

cnspec connects to Netlify using a personal access token. To create one:

  1. Log in to the Netlify app.
  2. Go to User settings > Applications > Personal access tokens.
  3. 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_TOKEN

cnspec 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

OptionDescription
--tokenNetlify personal access token for authentication
--accountScope discovery to a single Netlify account (slug or ID)
--discoverWhat 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_TOKEN
cnspec> 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_TOKEN

To scan a single account:

cnspec scan netlify --token YOUR_TOKEN --account acme

When 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_TOKEN

List 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: true

Ensure every account member has enrolled a second factor

cnspec> netlify.accounts.all(members.all(mfaEnabled == true))
[ok] value: true

Ensure no invitation stays outstanding

cnspec> netlify.accounts.all(members.none(pending == true))
[ok] value: true

Ensure 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: true

Ensure SAML sessions expire within a day

cnspec> netlify.accounts.where(samlEnabled == true).all(samlSessionExpiration <= 86400)
[ok] value: true

Ensure Netlify support can't administer the account

cnspec> netlify.accounts.all(supportAdministrationEnabled == false)
[ok] value: true

Ensure every site redirects visitors to HTTPS

cnspec> netlify.sites.all(forceSsl == true)
[ok] value: true

Ensure production can't be published outside of git

cnspec> netlify.sites.all(preventNonGitProdDeploys == true)
[ok] value: true

Ensure builds from untrusted contributors are reviewed

cnspec> netlify.sites.all(untrustedFlow == "review")
[ok] value: true

Ensure 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: true

Ensure 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: true

To 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: true

Review 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

On this page