Secret Management
Secret Management
Lucee 7 introduces built-in support for secrets management, allowing you to securely store and access sensitive information such as database credentials, API keys, and other confidential data. This feature helps maintain security best practices by keeping sensitive data out of application code and configuration files.
Configuration
In Lucee 7, secret providers can be configured similarly to datasources, caches, or AI connections, either in the Lucee Administrator (work in progress) or directly in .CFConfig.json
. Here are sample configurations:
Environment Variables Provider
Read the secret directly from the environment variables:
"secretProviders": {
"env": {
"class": "lucee.runtime.secrets.EnvVarSecretProvider",
"custom": {
"caseSensitive": false
}
}
}
File Provider
Read the secrets from a file, format can be json or env:
"secretProviders": {
"json": {
"class": "lucee.runtime.security.FileSecretProvider",
"custom": {
"type": "json",
"file": "/my/secrets/vars.json",
"caseSensitive": false
}
}
}
Combined Providers
The AndSecretProvider
allows you to combine multiple providers, checking each one in the order specified until a secret is found:
"secretProviders": {
"many": {
"class": "lucee.runtime.security.AndSecretProvider",
"custom": {
"providers": "env,json"
}
}
}
Using External Classes
Similar to other Lucee features, you can specify external classes for your secret providers using various methods:
OSGi Bundles
"secretProviders": {
"custom": {
"class": "com.mycompany.secrets.CustomSecretProvider",
"bundleName": "com.mycompany.secrets",
"bundleVersion": "1.2.0",
"custom": {
"configParam1": "value1",
"configParam2": "value2"
}
}
}
Maven Dependencies
Since Lucee 6.2, you can load classes directly from Maven repositories:
"secretProviders": {
"vault": {
"class": "com.company.secrets.VaultSecretProvider",
"maven": "com.company:vault-provider:1.0.0,com.company:common-utils:1.5.0",
"custom": {
"url": "https://vault.example.com",
"timeout": 5000
}
}
}
This allows you to specify one or more comma-separated Maven dependencies in gradle-style format.
CFML Components (Lucee 7+)
You can implement your own secret provider as a CFML component:
"secretProviders": {
"myCustom": {
"component": "path.to.MySecretProviderComponent",
"custom": {
"configParam1": "value1"
}
}
}
The component must implement lucee.runtime.secrets.SecretProvider
via implementsJava="lucee.runtime.secrets.SecretProvider"
annotation.
Supported Providers
Lucee includes several built-in secret providers:
- lucee.runtime.secrets.EnvVarSecretProvider: Reads secrets from environment variables
- lucee.runtime.security.FileSecretProvider: Reads secrets from a .json or .env file (more formats following)
- lucee.runtime.security.AndSecretProvider: Combines multiple providers into one
- AWSSecretsManagerProvider: Connects to AWS Secrets Manager (coming soon)
- GoogleSecretManagerProvider: Uses Google Cloud Secret Manager (coming soon)
- DockerSecretsProvider: Reads secrets from Docker secrets (coming soon)
Provider Configuration Options
EnvVarSecretProvider
Option | Description | Default | Notes |
---|---|---|---|
caseSensitive |
Determines if environment variable names are case-sensitive | true |
Set to false to allow case-insensitive lookups |
FileSecretProvider
Option | Description | Default | Notes |
---|---|---|---|
type |
File format | env |
Supported values: env , json |
file |
Path to the secrets file | None (Required) | Can use any Lucee-supported resource path (local, S3, HTTP, etc.) |
caseSensitive |
Determines if secret keys are case-sensitive | true |
Set to false to allow case-insensitive lookups |
refreshInterval |
How often to check for file changes (in milliseconds) | 60000 (1 minute) |
Set to 0 to disable auto-refresh |
AndSecretProvider
Option | Description | Default | Notes |
---|---|---|---|
providers |
Comma-separated list of provider names to check | None (Required) | Providers are checked in the specified order |
Using Secrets in Your Application
Secrets are accessed through the GetSecret()
function, which returns a reference to the secret value rather than the actual value itself.
Basic Usage
// Get a secret from a specific provider
apiKey = GetSecret("API_KEY", "env");
// Use the secret in an API call
cfhttp(url="https://api.example.com", method="GET") {
cfhttpparam(type="header", name="Authorization", value="Bearer #apiKey#");
}
Using Without Specifying Provider
// Get a secret without specifying a provider (checks all providers)
dbPassword = GetSecret("DB_PASSWORD");
// Use the secret in a database connection
dbConnection = {
host: GetSecret("DB_HOST"),
username: GetSecret("DB_USER"),
password: dbPassword
};
Storing References
// Store secret references at application startup
application.secrets = {
apiKey: GetSecret("API_KEY", "env"),
dbConfig: {
host: GetSecret("DB_HOST", "vault"),
username: GetSecret("DB_USER", "vault"),
password: GetSecret("DB_PASSWORD", "vault")
}
};
// Use them throughout the application
cfhttp(url="https://api.example.com", method="GET") {
cfhttpparam(type="header", name="Authorization", value="Bearer #application.secrets.apiKey#");
}
Key Features
Lazy Resolution
When you call GetSecret()
, it returns a reference to the secret rather than the actual value. The secret is only resolved to its actual value when it's used in a context that requires a simple value (string, boolean, number, date). This enables several powerful features:
- Auto-updating secrets: If a secret changes in the provider, your application automatically uses the updated value without needing to reload the application or reconfigure anything.
- Enhanced security: Secret values are not stored in memory until they're actually needed.
Obfuscation
Secret values are automatically obfuscated when dumped or displayed in debug output, reducing the risk of exposing sensitive information.
// This will display an obfuscated value rather than the actual secret
dump(GetSecret("API_KEY"));
Combined Providers
The AndSecretProvider
allows you to chain multiple providers together, checking each one in order until a secret is found. This enables fallback scenarios and tiered approaches to secret management.
// Configure a combined provider
// First checks environment variables, then AWS Secrets Manager
"secretProviders": {
"combined": {
"class": "lucee.runtime.security.AndSecretProvider",
"custom": {
"providers": "env,aws"
}
}
}
// Use the combined provider
apiKey = GetSecret("API_KEY", "combined");
Creating Custom Secret Providers
You can create your own secret provider by implementing the lucee.runtime.secrets.SecretProvider
interface. This can be done either in Java (compiled into a JAR and loaded via OSGi or Maven) or directly in CFML.
CFML Component Implementation
// MySecretProvider.cfc
component implementsJava="lucee.runtime.secrets.SecretProvider" {
// Initialize the provider with custom configuration
function init(struct config) {
variables.config = config;
return this;
}
// Get a secret by key
function getSecret(string key) {
// Implementation to retrieve the secret
// Return null if the secret doesn't exist
// Example implementation (database-stored secrets)
var q = queryExecute(
"SELECT value FROM app_secrets WHERE key = :key",
{key: key},
{datasource: config.datasource}
);
if (q.recordCount) {
return q.value;
}
return javaNull();
}
// Check if a secret exists
function hasSecret(string key) {
// Return true if the secret exists, false otherwise
// Example implementation
var q = queryExecute(
"SELECT COUNT(*) as count FROM app_secrets WHERE key = :key",
{key: key},
{datasource: config.datasource}
);
return q.count > 0;
}
}
To use this custom provider, configure it in your .CFConfig.json
:
"secretProviders": {
"database": {
"component": "path.to.MySecretProvider",
"custom": {
"datasource": "secretsDB"
}
}
}
Security Considerations
Secret Rotation
Most providers support automatic secret rotation. Because secrets are resolved when used rather than when retrieved, your application automatically uses the latest value of a secret without any code changes.
Secure Storage
Always ensure that your .env
files and other secret storage locations have appropriate file permissions and are excluded from version control systems.
Provider-Specific Security
Each provider has its own security considerations. For example:
- AWS Secrets Manager requires proper IAM roles and permissions
- Environment variables should be set securely through the operating system
- File-based secrets need strict file permissions
Troubleshooting
Debugging
Lucee logs every use of every key to the application
log with the log level trace
, this includes the stacktrace, the key used and the name of the secret provider itself.
When you enable that log level, you will see how and where your secrets get used (not the values themselves).
In addition, Lucee also logs in case it fails to load a secret provider.
Common Issues
- Secret not found: Ensure the secret exists in the provider and that the key is correct.
- Provider configuration: Verify that the provider is correctly configured and accessible.
- Permission issues: Check that the application has the necessary permissions to access the secrets.
- Class not found: When using external providers, ensure all required dependencies are available.
Best Practices
- Use namespacing for secrets: Organize secrets with clear naming conventions, such as
DB_USER
,DB_PASSWORD
, etc. - Store references, not values: Store references to secrets in application or session scope rather than resolved values.
- Implement least privilege: Only grant access to the specific secrets an application needs.
- Monitor usage: Regularly audit secret access and usage patterns.
- Layer providers: Use the
AndSecretProvider
to implement fallback mechanisms. - Environment segregation: Use different secret providers for development, staging, and production environments.
- Regular rotation: Rotate secrets regularly and verify that your application handles rotation gracefully.
Reference
GetSecret Function
GetSecret(key [, name])
- key: Key to read from the Secret Provider
- name: (Optional) Name of the Secret Provider to read from. If not provided, all configured providers are checked in order.
Returns a reference to the secret value that is automatically resolved when used in a context requiring a simple value.
SecretProvider Interface
package lucee.runtime.secrets;
public interface SecretProvider {
/**
* Initialize the provider with the given configuration
* @param config Provider-specific configuration
*/
public void init(java.util.Map<String, String> config);
/**
* Get a secret by key
* @param key Secret key
* @return Secret value or null if not found
*/
public String getSecret(String key);
/**
* Check if a secret exists
* @param key Secret key
* @return true if the secret exists, false otherwise
*/
public boolean hasSecret(String key);
}