Skip to content

7. Secrets Management and Security Scanning

The Salesforce Authentication Surface

Salesforce CI pipelines have a specific and well-defined authentication problem: the GitHub Actions runner needs to authenticate to one or more Salesforce orgs - sandboxes, scratch orgs, production - without storing credentials in the repository or passing them interactively. How this is solved has direct security implications that go beyond convenience.

The four patterns in common use, ordered from least to most recommended:

SFDX Auth URLs are the path of least resistance and the most dangerous. An SFDX auth URL encodes a refresh token, instance URL, and client credentials into a single string. If that string is committed to a repository - even briefly, even in a branch that is later deleted - it represents a persistent credential leak. Auth URLs should never be stored in repositories, should never be passed as plain environment variables in workflow YAML, and should be treated as equivalent in sensitivity to a private key. Their use in CI is discouraged; they are appropriate only for ephemeral local development flows.

Access/Opaque Token authentication is the simplest credential model - a Salesforce session token obtained from an authenticated org is stored directly as a GitHub Actions secret and passed to the Salesforce CLI via the SF_ACCESS_TOKEN environment variable or the --access-token flag. No Connected App configuration is required, and setup is minimal. The tradeoffs are significant enough that this approach should be considered a convenience option rather than a production governance pattern. Access tokens are session-based and expire - the default timeout is 2 hours but this is org-configurable, ranging from 15 minutes to 24 hours - meaning they require manual rotation or an automated refresh mechanism to remain valid in CI. They represent a specific user's authenticated session rather than a purpose-built service identity, which makes audit trail attribution less precise. For short-lived projects, developer sandboxes, or environments where Connected App configuration is not available, access token authentication is a practical starting point. For any environment mapped to a production or pre-production org, JWT Bearer Flow should be the standard.

JWT (JSON Web Token) Bearer Flow is the recommended authentication pattern for Salesforce CI pipelines. It requires a Connected App configured in the target org with a certificate-based authentication policy, and a private key stored as a GitHub Actions secret. The workflow authenticates by generating a signed JWT assertion - no refresh token, no interactive login, no long-lived session credential. The private key never leaves GitHub's secrets infrastructure during workflow execution. This pattern works across production orgs, sandboxes, scratch orgs, and Developer Edition orgs, and is the most widely supported headless authentication mechanism across the Salesforce ecosystem.

GitHub OIDC (OpenID Connect) is an emerging pattern that eliminates the need to store even the private key as a long-lived secret - the GitHub Actions runner obtains a short-lived OIDC token that Salesforce validates against a configured identity provider. This is architecturally the most secure option but requires Salesforce-side configuration that is not yet universally available across all org types. Teams operating at high security maturity should evaluate this pattern; for most organizations, JWT Bearer Flow provides a strong and practical baseline.

All Salesforce authentication and actions for long-lived environments should be performed as a dedicated service/integration user, not as any human. This allows better auditability.

Credential Storage - GitHub Secrets Architecture

GitHub Actions secrets are the baseline. Secrets are encrypted at rest, masked in logs, and scoped to either a repository or an organization. Organization-scoped secrets - where a single set of Connected App credentials is shared across all Salesforce repositories in the GitHub org - provides a clean centralized model. This is the recommended starting point for most teams.

GitHub Environments add an approval gate layer on top of secrets. Environment-scoped secrets are only accessible to workflows targeting that environment, and environments can be configured to require manual approval before a workflow step executes. This is the appropriate model for production deployment credentials - the production Connected App private key should live in a protected GitHub Environment, not a plain repository secret.

External Secret Vaults - HashiCorp Vault, AWS Secrets Manager, Azure Key Vault - are appropriate for organizations with existing enterprise secrets infrastructure or compliance requirements that mandate centralized credential management outside of GitHub. The tradeoff is operational overhead: vault integration adds pipeline complexity and an external dependency that must be maintained.

Secret Scanning - Preventing Credential Leaks

Pre-commit secret scanning should be wired into Husky pre-commit hooks so that credential patterns are intercepted before they enter Git history - the earliest possible intervention point. A number of open source and commercial tools support this pattern, including Gitleaks (GitHub) and TruffleHog (GitHub). The choice of tool is less important than ensuring that the same credential pattern definitions govern both local pre-commit scanning and CI-time scanning, so that no divergence exists between what a developer's machine detects and what the pipeline enforces. The pattern definitions should be version-controlled and centralized in the configuration repository so they are consistent across all project repositories.

CI-time scanning runs the same tooling as a workflow step on every push and pull request, providing a safety net for contributors who have not configured local hooks or are working in environments where pre-commit hooks are bypassed. GitHub's native secret scanning - available on GitHub Advanced Security - provides an additional layer that operates across the entire repository history, not just new commits, and can alert on secrets detected in any branch.

The combination of pre-commit and CI-time scanning creates a defense-in-depth model where no single point of failure results in an undetected credential leak.

Branch Protection and Signed Commits as a Policy Layer

Branch protection rules and signed commit requirements are the GitHub-native mechanism for enforcing repository integrity as a policy, independent of individual contributor behavior.

At minimum, the main branch and any environment-mapped branches should be protected with: PR required with review, required status checks that must pass before merge, and restrictions on force pushes and branch deletion.

Signed commits - requiring that all commits carry a verified cryptographic signature - provide an additional integrity guarantee. GitHub's Vigilant Mode surfaces unsigned commits visually in the repository interface. Enforcement via GitHub Rulesets can make signed commits a hard requirement for protected branches. The implementation details of GPG or SSH signing are outside the scope of this whitepaper, but the policy decision should be made explicitly and documented in the organization's GitHub governance standards rather than left to individual teams.

Dependency Scanning - npm and Managed Packages

npm dependencies introduced by LWC development and used as sfdx devDependencies are the higher-velocity risk surface. Three complementary controls address this:

Dependabot - enabled via a dependabot.yml configuration file - automatically opens pull requests when npm dependencies have available updates, including security patches. It is GitHub-native, requires no installation, and is the lowest-friction starting point. Dependabot should be configured for both the npm and github-actions ecosystems - workflow action dependencies carry their own supply chain risk and are frequently overlooked.

  • Documentation: https://docs.github.com/en/code-security/dependabot

Mend Renovate is the more configurable alternative - and for organizations managing multiple Salesforce repositories, the more appropriate choice at scale. Renovate supports organization-wide shared configuration presets via a central renovate.json file, enabling consistent dependency update policies across all repositories. Its update grouping capability can batch related dependencies such as all @salesforce/* scoped packages into a single PR. Renovate is available as a free hosted GitHub App via Mend, or self-hosted. Some teams run both tools - Renovate for routine version updates with grouping and scheduling, Dependabot retained for its native GitHub security alert integration.

  • GitHub: https://github.com/renovatebot/renovate
  • Documentation: https://docs.renovatebot.com/

Internal shared packages - such as a centralized Prettier configuration published as @<my-org>/prettier-config (described in Section 5) - are npm dependencies in the same way as any third-party package, and should be tracked by Dependabot and Renovate alongside external dependencies, ensuring that repositories consuming a pinned version of a shared config package receive automated PRs when a new version is published.

RetireJS via Salesforce Code Analyzer scans LWC components and static resources for known vulnerable JavaScript dependencies as part of quality gates, providing a complementary signal to Dependabot and Renovate's update-focused approach - catching vulnerabilities in dependencies that are present in the codebase but have not yet triggered an update PR.

  • GitHub: https://github.com/RetireJS/retire.js

Managed packages are a lower-velocity but harder-to-govern risk surface. Installed managed packages are outside the repository's dependency graph - they do not appear in package.json - and package upgrades in a production org can introduce behavioral changes silently. Primary controls are process-based: documenting installed packages and versions in a versioned artifact, requiring change management approval for package upgrades, and running the full local test suite against the org after any package upgrade and before promoting further changes.

Further reading: