I have recently created a managed package and I am preparing it for security review and final deployment. In the code I need to sign JWT used to authenticate against external API, for this purpose I am using a custom metadata object with text fields for storing my cryptographic keys. Public, secret, etc. However, I have encountered a problem. The Source of truth for 2GP package is version control. That means, every custom object, apex class, custom metadata will be available for developers with access to git repository. This secret keys exposure is a major security flaw.

What is the best way to deploy custom metadata securely, avoid storing it in .xml file in a repository, and limit keys exposure?

1 Answer 1


You'll need to store the secrets in environment variables in your build system, and inject them just prior to initiating uploads of 2GP managed package versions. Your source tree would contain only some arbitrary token that is the replacement target.

I'll keep this example somewhat abstract to account for differences in what CI or build system you actually use, but suppose something like this:


This is the record of your Custom Metadata Type that stores an API or cryptographic key.

<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <value xsi:type="xsd:string">{{ SECRET_KEY }}</value>

That is, you store in your metadata some sigil value that you can easily target in a script for replacement, but which is not itself secret.

Build System Environment Variables

Your real keys would live in secure environment variables (build/CI systems call these various things, but they all have something equivalent). For example, you'd create an environment variable or secret SECRET_KEY with the actual value of your crypto key.

Build Script

The script you use will look different based on what your build system actually is. But fundamentally, what it'll do is, immediately prior to running sfdx force:package:version:upload, grab the value of your key(s) from the environment variables or build secrets and substitute them into the files in place of the sigil values.

Then, your secrets will be accessible only in the build system, and will be guarded by the platform's Custom Metadata protection at all other times inside the boundary of the managed package.

Caveats and Notes

This only works if your package version uploads are always done in a CI or build system - but if you don't do that, you can't protect your key.

It also means that your package, when under development by your team, won't have API access. You may be able to define alternate development keys and inject those instead when your developers create work orgs through local scripting. For example, developers could have keys in local .envrc files they manage with direnv and do not commit to source control, and a local scratch org creation script could inject them into the metadata before building an org and then revert the files in the local source tree afterwards.

One Implementation

I support multiple teams at Salesforce.org that use this workflow. In our case, it's implemented using CumulusCI (which my team builds and maintains). CumulusCI uses a flow (an orchestration of tasks, not an on-platform Flow) called release_2gp_beta, which we can customize by adding a find-replace task immediately prior to the version upload. It looks something like this:

         class_path: cumulusci.tasks.util.FindReplace
             find: "{{ SECRET_KEY }}"
             env_replace: SECRET_KEY
             path: force-app/main/default/customMetadata
                task: inject_key

Since all package versions are created on our build system, this allows us to fully protect keys. Similar customizations to flows like dev_org (for building a development scratch org) allow the injection of developers' local keys.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .