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_SECRETAn 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_SECRETWhen these are set, you can omit the matching flags from the commands below.
Connection options
| Option | Description |
|---|---|
--account-id | Zoom account ID |
--client-id | Server-to-Server OAuth app client ID |
--client-secret | Server-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_SECRETcnspec> 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_SECRETWhen 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_SECRETReview 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: trueEnsure meetings require a passcode
cnspec> zoom.account.meetingPasscodeRequired == true
[ok] value: trueEnsure 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: trueEnsure participants must authenticate before joining
cnspec> zoom.account.meetingAuthenticationRequired == true
[ok] value: trueEnsure end-to-end encryption is available to hosts
cnspec> zoom.account.meetingE2eeAvailable == true
[ok] value: trueEnsure 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: trueEnsure 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: trueEnsure single sign-on is enabled
cnspec> zoom.account.sso.enabled == true
[ok] value: trueEnsure 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: trueEnsure no group weakens the waiting room
cnspec> zoom.groups.all(settingsWaitingRoomEnabled == true)
[ok] value: trueEnsure no group drops the meeting passcode
cnspec> zoom.groups.all(settingsMeetingPasscodeRequired == true)
[ok] value: trueEnsure no group opens its meetings to unauthenticated participants
cnspec> zoom.groups.all(settingsOnlyAuthenticatedUsersCanJoin == true)
[ok] value: trueEnsure 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: trueEnsure every user's email address is verified
cnspec> zoom.users.all(verified == true)
[ok] value: trueReview 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
- Zoom Resource Pack Reference: every Zoom resource and field cnspec can query
- Write Effective MQL: guide to authoring checks and queries