Secure JFrog Artifactory with cnspec
Scan JFrog Artifactory instances against security and compliance best practices with cnspec.
Artifactory is often the only registry a platform installs from, so a permission mistake there reaches every consumer of the artifacts it serves. cnspec evaluates which repositories exist and what a remote one proxies, which principals hold which actions on which repository patterns, whether an unauthenticated caller can read or publish, who holds administrative rights, which access tokens never expire, every way an account can be created through an identity integration, and where the instance writes its scheduled exports.
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 Artifactory with cnspec, you must have:
- cnspec installed on your workstation
- A JFrog Artifactory instance, either cloud or self-hosted
- An Artifactory access token, or the legacy API key
Authenticate
cnspec connects to Artifactory with an access token. To create one, open the JFrog platform and go to User Menu > Edit Profile > Generate an Identity Token. You can also create one with the JFrog CLI:
jf atcAdministrative rights
Most of what cnspec reads from Artifactory is available only to an administrator:
| Resource | Rights needed |
|---|---|
repositories, system | Any account |
permissionTargets | Administrator |
users, groups | Administrator |
security, and the identity integrations under it | Administrator |
backups | Administrator |
cleanupPolicies | Administrator, and a product version that serves the endpoint |
accessTokens | Administrator to see every token, otherwise only the caller's own |
A token that lacks those rights reports the field as an error rather than as an empty result, so an audit never passes on data cnspec was never allowed to read. If you want full coverage, issue the token from an administrator account.
Instance URL
Pass the JFrog platform base URL. A cloud instance is https://YOUR_NAME.jfrog.io and a self-hosted instance is the host the platform is served on. cnspec accepts the /artifactory suffix that the web interface shows and removes it for you. A URL can also be passed as a bare argument instead of with the --url flag:
cnspec shell artifactory https://example.jfrog.io --token YOUR_TOKENEnvironment variables
You can export the connection details once and reuse them across commands:
export ARTIFACTORY_URL=https://example.jfrog.io
export ARTIFACTORY_TOKEN=YOUR_TOKENcnspec also accepts JFROG_URL for the URL, JFROG_ACCESS_TOKEN for the token, and ARTIFACTORY_API_KEY for the legacy API key. When these are set, you can omit the matching flags from the commands below.
Connection options
| Option | Description |
|---|---|
--url | JFrog platform base URL, for example https://example.jfrog.io |
--token | Artifactory access token for authentication |
--api-key | Artifactory API key, an alternative to the access token |
Verify with a quick Artifactory check
Confirm that cnspec can reach your instance by opening a cnspec shell:
cnspec shell artifactory --url https://example.jfrog.io --token YOUR_TOKENcnspec> artifactory.system { version serviceId addons }
artifactory.system: {
version: "7.90.10"
serviceId: "jfrt@01ab2c3d4e5f6g7h8i9j0k"
addons: [
0: "build"
1: "docker"
]
}If cnspec connects and reports your version, you're ready to scan.
Scan Artifactory
cnspec scan artifactory --url https://example.jfrog.io --token YOUR_TOKENWhen 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 Artifactory 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 artifactory --url https://example.jfrog.io --token YOUR_TOKENList all repositories
cnspec> artifactory.repositories { key type packageType blackedOut xrayIndex }
artifactory.repositories: [
0: {
key: "docker-local"
type: "local"
packageType: "docker"
blackedOut: false
xrayIndex: true
}
]Find repositories an unauthenticated caller can use
anonymousActions lists what the anonymous user holds on the repository, so an empty list means anonymous callers get nothing:
cnspec> artifactory.repositories.where(anonymousActions.length > 0) { key packageType anonymousActions }Review the instance security settings
cnspec> artifactory.security {
anonymousAccessEnabled
anonymousCanRead
anonymousCanDeploy
hideUnauthorizedResources
userLockPolicyEnabled
loginAttempts
passwordExpiryEnabled
buildGlobalBasicReadAllowed
buildGlobalBasicReadForAnonymous
}List administrators
cnspec> artifactory.users.where(admin == true) { name email realm lastLoggedIn }Inspect permission targets and who they name
A permission target grants principals actions over repository path patterns, and principals flattens every principal the target names across all of its scopes:
cnspec> artifactory.permissionTargets { name principals { name type actions canDeploy canManage } }Find grants that cover every repository
A scope with appliesToAllRepositories uses a wildcard key, so it reaches repositories that didn't exist when the grant was written:
cnspec> artifactory.permissionTargets.where(repo.appliesToAllRepositories == true) { name repo { includePatterns } }List access tokens and what they carry
cnspec> artifactory.accessTokens { id subject grantsAdmin expires expiry refreshable }Review the identity integrations
Every enabled integration is a separate way to become a principal on the instance, so the set of them bounds who can sign in at all. saml, oauth, httpSso, and crowd return null when the instance has no such configuration:
cnspec> artifactory.security.ldapSettings { key enabled ldapUrl usesEncryptedTransport autoCreateUser }
cnspec> artifactory.security.saml { enabled noAutoUserCreation autoRedirect verifyAudienceRestriction hasCertificate syncGroups }
cnspec> artifactory.security.oauth { enabled persistUsers providers { name providerType enabled } }
cnspec> artifactory.security.httpSso { httpSsoProxied remoteUserRequestVariable noAutoUserCreation }
cnspec> artifactory.security.crowd { enabled serverUrl noAutoUserCreation sessionValidationInterval }Review which directory groups map onto instance groups
cnspec> artifactory.security.ldapGroupSettings { name strategy groupBaseDn subTree ldapSetting { key enabled } }Review scheduled exports
An export contains the artifacts and the configuration, including the secrets held in it, so where it's written and how long it's kept are part of the instance's exposure:
cnspec> artifactory.backups { key enabled cronExpression retentionPeriodHours excludeNewRepositories excludedRepositories }Review cleanup policies
cnspec> artifactory.cleanupPolicies { key enabled cronExpression repositories keepLastNVersions skipTrashcan }Walk from a resource to its neighbors
Repositories, users, groups, and permission targets reference each other, so a query can start from any of them and walk in either direction:
cnspec> artifactory.repositories { key permissionTargets { name } }
cnspec> artifactory.groups { name users { name admin } }
cnspec> artifactory.users { name permissionTargets { name } }
cnspec> artifactory.accessTokens { id subjectRef { name admin } }Example security checks
Ensure anonymous access is disabled
cnspec> artifactory.security.anonymousAccessEnabled == false
[ok] value: trueEnsure an unauthenticated caller can't publish artifacts
Anonymous publish rights let anyone replace the artifacts your platform installs:
cnspec> artifactory.security.anonymousCanDeploy == false
[ok] value: trueEnsure no permission target grants anonymous publish
The instance-wide setting isn't the only way anonymous publish gets in. A permission target can name the anonymous user directly, and cnspec lists the targets that would apply whether or not anonymous access is on, so you can see what turning it on would immediately allow:
cnspec> artifactory.security.anonymousDeployTargets.length == 0
[ok] value: trueEnsure no repository exposes actions to the anonymous user
cnspec> artifactory.repositories.all(anonymousActions.length == 0)
[ok] value: trueEnsure build info isn't readable instance-wide
A global build read bypasses the permission targets, so a build that names its artifacts and its environment becomes visible to every account:
cnspec> artifactory.security.buildGlobalBasicReadAllowed == false
[ok] value: trueEnsure build info isn't readable without an account at all
cnspec> artifactory.security.buildGlobalBasicReadForAnonymous == false
[ok] value: trueEnsure accounts lock after repeated failed sign-ins
cnspec> artifactory.security.userLockPolicyEnabled == true
[ok] value: trueEnsure the lockout threshold is strict
cnspec> artifactory.security.loginAttempts <= 5
[ok] value: trueEnsure internal passwords expire
cnspec> artifactory.security.passwordExpiryEnabled == true
[ok] value: trueEnsure administrative rights stay rare
Every administrator can rewrite any grant on the instance, so the count belongs under review:
cnspec> artifactory.users.where(admin == true).length <= 3
[ok] value: trueEnsure no group hands out administrative rights automatically
A group that is both autoJoin and adminPrivileges makes every new account an administrator:
cnspec> artifactory.groups.none(autoJoin == true && adminPrivileges == true)
[ok] value: trueEnsure no administrative token lives forever
cnspec> artifactory.accessTokens.none(grantsAdmin == true && expires == false)
[ok] value: trueEnsure every access token expires
cnspec> artifactory.accessTokens.all(expires == true)
[ok] value: trueEnsure every LDAP server is reached over an encrypted connection
A URL with the ldap scheme rather than ldaps carries the bind credential over an unencrypted connection:
cnspec> artifactory.security.ldapSettings.where(enabled == true).all(usesEncryptedTransport == true)
[ok] value: trueEnsure a directory bind can't create accounts on the instance
When a successful bind creates an account, every principal in the directory becomes a principal here:
cnspec> artifactory.security.ldapSettings.where(enabled == true).none(autoCreateUser == true)
[ok] value: trueEnsure a SAML assertion can't create an account
Run this where the instance uses SAML. When noAutoUserCreation is false, every principal the identity provider accepts becomes a principal here:
cnspec> artifactory.security.saml.noAutoUserCreation == true
[ok] value: trueEnsure a SAML assertion minted for another service can't be replayed
Without the audience restriction check, an assertion issued for a different service provider is accepted here:
cnspec> artifactory.security.saml.verifyAudienceRestriction == true
[ok] value: trueEnsure the SAML identity provider has a signing certificate
Without one, an assertion can't be verified against the provider that claims to have issued it:
cnspec> artifactory.security.saml.hasCertificate == true
[ok] value: trueEnsure an OAuth sign-in can't create an account
cnspec> artifactory.security.oauth.persistUsers == false
[ok] value: trueEnsure a proxy header can't create the account it names
HTTP SSO takes the account name from a request header, so the instance is only as strong as the proxy in front of it. A caller that reaches the instance directly can set the header itself:
cnspec> artifactory.security.httpSso.noAutoUserCreation == true
[ok] value: trueEnsure every repository is indexed for vulnerabilities
cnspec> artifactory.repositories.all(xrayIndex == true)
[ok] value: trueEnsure remote repositories don't leak credentials
allowAnyHostAuth lets a remote repository send its credentials to any host it gets redirected to:
cnspec> artifactory.repositories.all(allowAnyHostAuth == false)
[ok] value: trueEnsure remote repositories can't resolve dependencies from anywhere
cnspec> artifactory.repositories.all(externalDependenciesEnabled == false)
[ok] value: trueEnsure a caller learns nothing about what it may not read
cnspec> artifactory.security.hideUnauthorizedResources == true
[ok] value: trueEnsure no export is kept forever
An export that's kept forever accumulates every secret the instance held at each point in time:
cnspec> artifactory.backups.where(enabled == true).all(retentionPeriodHours != null)
[ok] value: trueEnsure a new repository isn't silently left out of the export
cnspec> artifactory.backups.where(enabled == true).none(excludeNewRepositories == true)
[ok] value: trueReview accounts that aren't active
An account left in a locked or disabled state still holds its group memberships and grants:
cnspec> artifactory.users.where(status != "enabled") { name status realm lastLoggedIn }Learn more
- JFrog Artifactory Resource Pack Reference: every Artifactory resource and field cnspec can query
- Write Effective MQL: guide to authoring checks and queries