SaaS

Secure Zoom with cnspec

Scan Zoom accounts, users, roles, and groups against security and compliance best practices with cnspec.

Every meeting on a Zoom account inherits the account's defaults, so one setting decides whether a stranger with the meeting ID can walk into any conversation your company holds. cnspec reads a Zoom account through Server-to-Server OAuth and evaluates the meeting security defaults applied account wide, whether cloud recordings are encrypted where they're stored, how single sign-on provisions the workforce, which users hold admin equivalent roles, and which group overrides put part of the workforce on weaker settings than the account itself.

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.

The Zoom resources are experimental. Field names and behavior can change in a future cnspec release.

Prerequisites

To scan Zoom with cnspec, you must have:

  • cnspec installed on your workstation
  • A Zoom account on a plan that offers Server-to-Server OAuth apps
  • A Server-to-Server OAuth app with read scopes for account settings, users, roles, and groups

cnspec only issues read requests to the Zoom API. It never changes an account.

Authenticate

cnspec authenticates as the account itself, not as a person, using a Server-to-Server OAuth app. To create one, open the Zoom App Marketplace, go to Develop > Build App, and choose Server-to-Server OAuth. Grant the app read scopes for account settings, users, roles, and groups, then activate it and copy the account ID, client ID, and client secret from the app's credentials page.

cnspec shell zoom --account-id YOUR_ACCOUNT_ID --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET

An app that lacks a scope reports the fields it covers as an error rather than as an empty result, so an audit never passes on data cnspec was never allowed to read.

Environment variables

ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, and ZOOM_CLIENT_SECRET supply the same values as the flags:

export ZOOM_ACCOUNT_ID=YOUR_ACCOUNT_ID
export ZOOM_CLIENT_ID=YOUR_CLIENT_ID
export ZOOM_CLIENT_SECRET=YOUR_CLIENT_SECRET

When these are set, you can omit the matching flags from the commands below.

Connection options

OptionDescription
--account-idZoom account ID
--client-idServer-to-Server OAuth app client ID
--client-secretServer-to-Server OAuth app client secret

Verify with a quick Zoom check

Confirm that cnspec can reach your account by opening a cnspec shell:

cnspec shell zoom --account-id YOUR_ACCOUNT_ID --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
cnspec> zoom.account { id accountName ownerEmail }
zoom.account: {
  id: "aBcD1234EfGh5678IjKl"
  accountName: "Lunalectric"
  ownerEmail: "it-admin@lunalectric.com"
}

If cnspec connects and reports your account, you're ready to scan.

Scan Zoom

cnspec scan zoom --account-id YOUR_ACCOUNT_ID --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET

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 Zoom 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 zoom --account-id YOUR_ACCOUNT_ID --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET

Review the meeting security defaults

These settings apply to every meeting the account hosts unless a group override replaces them:

cnspec> zoom.account {
    meetingWaitingRoomEnabled
    meetingPasscodeRequired
    meetingPmiPasscodeRequired
    meetingAuthenticationRequired
    meetingOnlyAccountUsersCanJoin
  }

Review meeting encryption

meetingEncryptionType is enhanced_encryption, where Zoom holds the keys, or end_to_end_encryption, where only the participants do. meetingE2eeAvailable reports whether hosts can select end to end encryption at all:

cnspec> zoom.account { meetingEncryptionType meetingE2eeAvailable }

Review cloud recording

A cloud recording keeps the meeting long after it ends, so where it lives and how it's protected outlast the conversation:

cnspec> zoom.account { cloudRecordingEnabled cloudRecordingEncryptionEnabled }

Review the single sign-on configuration

cnspec> zoom.account.sso { enabled domains groupMappingEnabled idpIssuer idpSsoUrl }

List users and how they sign in

loginType reports the sign-in method the user last authenticated with, and ssoLinked reports whether the account's identity provider governs them:

cnspec> zoom.users { email type status loginType ssoLinked lastLoginTime }

List roles and the privileges they carry

cnspec> zoom.roles { name description totalMembers privileges }

Find who holds an admin equivalent role

cnspec> zoom.roles.where(name == "Owner" || name == "Admin") { name totalMembers members { email lastLoginTime } }

Review the group overrides

A group's settings take precedence over the account defaults for its members, so a group can put part of the workforce on weaker meeting security than the account as a whole:

cnspec> zoom.groups {
    name
    totalMembers
    settingsWaitingRoomEnabled
    settingsMeetingPasscodeRequired
    settingsE2eeAvailable
    settingsOnlyAuthenticatedUsersCanJoin
  }

Walk from a resource to its neighbors

Users, roles, and groups reference each other, so a query can start from any of them and walk in either direction:

cnspec> zoom.users { email role { name privileges } }
cnspec> zoom.groups { name members { email status } }
cnspec> zoom.roles { name members { email ssoLinked } }

Example security checks

Ensure the waiting room is on by default

Without a waiting room, anyone who reaches the meeting is already in it:

cnspec> zoom.account.meetingWaitingRoomEnabled == true
[ok] value: true

Ensure meetings require a passcode

cnspec> zoom.account.meetingPasscodeRequired == true
[ok] value: true

Ensure a Personal Meeting ID requires a passcode

A Personal Meeting ID never rotates, so one leak reaches every meeting held on it:

cnspec> zoom.account.meetingPmiPasscodeRequired == true
[ok] value: true

Ensure participants must authenticate before joining

cnspec> zoom.account.meetingAuthenticationRequired == true
[ok] value: true

Ensure end-to-end encryption is available to hosts

cnspec> zoom.account.meetingE2eeAvailable == true
[ok] value: true

Ensure cloud recordings are encrypted where they're stored

Run this where cloud recording is enabled. A recording holds everything the meeting discussed and everything it showed on screen:

cnspec> zoom.account.cloudRecordingEncryptionEnabled == true
[ok] value: true

Ensure a sign-in session expires

A timeout of 0 means the session never expires, so a signed-in client stays signed in for as long as it's kept:

cnspec> zoom.account.signInSessionTimeoutMinutes > 0
[ok] value: true

Ensure single sign-on is enabled

cnspec> zoom.account.sso.enabled == true
[ok] value: true

Ensure every user signs in through single sign-on

Run this where the account uses SSO. A user who signs in another way holds a credential your identity provider can't revoke:

cnspec> zoom.users.all(ssoLinked == true)
[ok] value: true

Ensure no group weakens the waiting room

cnspec> zoom.groups.all(settingsWaitingRoomEnabled == true)
[ok] value: true

Ensure no group drops the meeting passcode

cnspec> zoom.groups.all(settingsMeetingPasscodeRequired == true)
[ok] value: true

Ensure no group opens its meetings to unauthenticated participants

cnspec> zoom.groups.all(settingsOnlyAuthenticatedUsersCanJoin == true)
[ok] value: true

Ensure admin equivalent access stays rare

Every account owner and admin can change the account defaults every meeting inherits, so the count belongs under review:

cnspec> zoom.roles.where(name == "Admin").all(totalMembers <= 3)
[ok] value: true

Ensure every user's email address is verified

cnspec> zoom.users.all(verified == true)
[ok] value: true

Review accounts nobody has signed in to for 90 days

A dormant account still holds its role and its license:

cnspec> zoom.users.where(lastLoginTime < time.now - 90*time.day) { email lastLoginTime role { name } }

Learn more

On this page