Bench Notes

Securing Salesforce DevOps: Least Privilege Access Control

Written by Jason Lantz | Sep 4, 2024 1:36:02 PM

As Salesforce developers, we're building some of the most critical applications for our businesses. We've embraced DevOps practices to speed up our development and delivery processes. But in our rush to automate, have we overlooked some fundamental security principles?

Today, let's talk about a principle that's often violated in Salesforce CI/CD setups: the principle of least privilege access control.

The Scary World of CI/CD Credentials

Let's start with a somewhat alarming statement: CI/CD credentials are the scariest credentials in your Salesforce ecosystem. Why? These credentials often grant full admin rights to enable metadata deployments and configuration of orgs through builds. For packaging, allowing a developer to extract the package promotion credential is essentially giving them direct access and control over the path to production - a key control in SOC compliance.

Yet, many Salesforce teams are using overly broad credentials in their CI/CD builds. It's time we took a closer look at this practice.

Temporary Access: The Gold Standard

Here's a fundamental truth about build jobs: they typically only need temporary access to the target org during the job duration. If something is needed from another org for the job, it should be fetched via another job for that org.

But how many of us are actually implementing this? More often than not, we're giving our build jobs perpetual access to our orgs. This is a violation of the principle of least privilege, and it's putting our systems at unnecessary risk.

"My sfdxAuthUrl is in GitHub Secrets so it's protected, right?"

I hear this a lot, and I get it. It feels secure. But let's break down what's actually in that sfdxAuthUrl:

  • Client ID
  • Client Secret
  • Refresh Token
  • Instance URL

That's everything needed to get a fresh OAuth access token at any point in the future. Even worse, if you're using a custom Connected App with Salesforce CLI, you're exposing the client id and client secret of that app.

Let me demonstrate how easy it is to exploit this. Here's a simple command to parse the sfdxAuthUrl and use curl to refresh the token:

bash
parsed_url=$(echo $SFDX_AUTH_URL | base64 --decode)
client_id=$(echo $parsed_url | sed 's/.*client_id=\([^&]*\).*/\1/')
client_secret=$(echo $parsed_url | sed 's/.*client_secret=\([^&]*\).*/\1/')
refresh_token=$(echo $parsed_url | sed 's/.*refresh_token=\([^&]*\).*/\1/')
instance_url=$(echo $parsed_url | sed 's/.*instance_url=\([^&]*\).*/\1/')

curl $instance_url/services/oauth2/token -d grant_type=refresh_token -d client_id=$client_id -d client_secret=$client_secret -d refresh_token=$refresh_token

With this simple script, anyone who gets access to your sfdxAuthUrl can perpetually access your org. Scary, right?

The DevHub Dilemma

Another common practice that violates least privilege is granting DevHub access via a single high-privilege credential. DevHub comes with Limited Access - Free user licenses designed to be used for service accounts with different permissions or by developers who don't need a full Salesforce license.

Instead of a single all-powerful credential, consider setting up multiple users with specific permissions:

  1. ScratchOrgOnly: A user with permissions only to create/manage scratch orgs
  2. Packaging: A user with permissions to create packages and package versions but not promote them
  3. PackagePromotion: A user with only the permission to promote packages

This granular approach significantly reduces the potential damage if any single credential is compromised.

The Static sfdxAuthUrl Problem

There's another issue with our current setup: the use of a static sfdxAuthUrl used by each worker to refresh the token. This approach blocks the implementation of an important security feature on Salesforce Connected Apps: refresh token rotation.

Ideally, each time a refresh token is used, a new one should be issued. But our current setup doesn't allow the worker to write back to the secret it was given (nor should it be able to). This leaves us stuck with long-lived refresh tokens, increasing our vulnerability.

Moving Towards Least Privilege

So, how do we fix this? Here are a few steps to move towards least privilege access control:

  1. Use temporary, job-specific credentials whenever possible
  2. Implement granular DevHub access with multiple limited-access users
  3. Avoid storing long-lived tokens like sfdxAuthUrl in your CI/CD system
  4. Implement a secure token vending system for your CI/CD jobs
  5. Regularly audit and rotate your credentials

Implementing these changes isn't trivial, but the security benefits are substantial. Remember, in the world of DevOps, speed shouldn't come at the cost of security.

In the next post in this series, we'll dive deeper into implementing these principles in practice. Stay tuned, and in the meantime, take a hard look at your CI/CD credential practices. Are you following the principle of least privilege?