Environment Secrets
Environment secrets are stored in AWS Secrets Manager within your AWS account and can be accessed by your application using the AWS SDK.
Stackery namespaces each secret with the name of the environment in which the secret was created. This namespace value is available to your application code (Lambda or Fargate task) via an environment variable called SECRETS_NAMESPACE
.
For example, a secret named 'important-secret' saved in the 'development' environment will be saved to AWS Secrets Manager as '/development/important-secret'.
A Secret's namespace includes the leading and trailing forward slash that encapsulates it.
Key Design Considerations
The twelve-factor app manifesto recommends the use of environment variables to enable strict separation of config from code. While environment variables are better than config files that are susceptible to being checked into source code, industry experts agree that environment variables are not a good fit for sensitive information such as database credentials, API keys, SSH private keys, TLS certificates, or any other data that needs to be kept confidential because the risk of accidentally exposing environment variables is high. A few situations that cause this increase in risk are:
- Applications that grab the whole environment and print for debugging or error reporting
- Environment variables that are passed down to child processes and used in unintended ways
- Application crashes that result in environment variables getting saved to log files for later debugging (ie: plain-text secrets being written to files)
- Attackers that steals environment variables by hacking applications or uploading malicious packages into popular package repositories
AWS Secrets Manager enables customer to rotate, manage, and retrieve sensitive information throughout their lifecycle:
- Admins and Developers can store and manage access to secrets securely with fine grained access policies
- Organizations can centrally audit and monitor the use of secrets and rotate them without risk of breaking applications
- Application code can seamlessly retrieve authorized secrets from Secrets Manager at runtime
Use Case
Accessing an API key from application code
Example: Save a secret and write application code to retrieve the secret at runtime.
- Application owner saves API key credentials as a secret within each of the various Stackery environments the application gets deployed to
- Application code utilizes AWS SDK to call out to AWS Secrets Manager for retrieval of secret
- Stackery automatically creates the IAM role that allows the application to retrieve the secret from AWS Secrets Manager at runtime
Security
The Stackery Role that is provisioned in your AWS account includes a tightly scoped IAM role that grants only the following actions to Stackery:
- CreateSecret
- DeleteSecret
- ListSecrets
- PutSecretValue
- RestoreSecret
- TagResource
- UpdateSecret
Stackery is not granted the GetSecretValue action. Furthermore, Stackery never saves or persists the value of the secret created via the Stackery Dashboard or CLI.
Using the Stackery CLI to Manage Secrets
The Stackery CLI can be used to create, delete, list, and set secrets. See the stackery env secrets section of the docs to review the available commands and examples.
Using the Stackery Dashboard to Manage Secrets
Navigate to the Environments section of the Stackery Dashboard and choose an environment to associate the secret with. The Add New Secret button at the bottom right allows you to create and configure a new secret, while the ellipses to the right of each existing secret allow you to edit description, set value, or delete a secret.
Adding a new secret
When you click Add New Secret, a modal will pop up allowing you to specify options for your new secret. These include:
- The secret name (cannot contain whitespace)
- The value of the secret
- The description of the secret (default description is provided but can be overwritten by typing a description)
Generated ID
and Description
will automatically mimic the input of your secret's Name
, and you'll end up with configurations similar to the one below.
Using secrets
Once a secret has been created you'll be able to identify it under Secret Name, which is preceded by the current environment's namespace. Each entry displays a description of your secret, as well as a link to the secret inside the AWS Secrets Manager console.
In the Stackery Dashboard you provide Function or Docker Task resources access to your Environment Secrets by adding a Secrets resource to the Stack Editor.
As a result of connecting the Secrets resource to a Function, using a service discovery wire, the SECRETS_NAMESPACE environment variable will populate in the Function's configurable properties. The IAM policy that grants the compute resource access to AWS Secrets Manager is also populated with a service discovery wire.
The IAM policy and Secrets Namespace environment variables are essential for retrieving secrets in AWS Secrets Manager.
Deleting a secret
To delete an Environment Secret, simply click the context menu icon for that secret, and select Delete.
When deleting a Secret in the Dashboard, AWS Secrets Manager schedules a secret for deletion rather than immediately deleting it. The minimum time before deletion is 7 days, so the secret still exists under its previous name and is restorable. Users can delete secrets immediately and without a recovery window using the CLI or SDK. See the
stackery env secrets delete
command.
How to fetch your secrets
The following code snippets are examples of the required AWS Secrets Manager API calls made within a Functions's handler function. Each example below fetches the apiKey
stored in AWS Secrets Manager.
Some code in the following examples has been omitted to not distract from the AWS Secrets Manager API calls this section covers. Full Python, Java, and .NET Core examples are linked below
// Import AWS SDK and instantiate a variable for the AWS Secrets Manager
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
// Store the SECRETS_NAMESPACE value from the Function's environment variables
const secretsNamespace = process.env.SECRETS_NAMESPACE;
exports.handler = async message => {
// Construct paramaters to pass to AWS Secrets Manager API call
// SecretId is a combination of the secret's namespace and the specific secret to return
const params = {
SecretId: secretsNamespace + 'apiKey'
};
// AWS Secrets Manager API call passing through params for retrieval
const response = await secretsManager.getSecretValue(params).promise();
// Accessing the secret's value of the response object
const apiKey = response.SecretString;
return {};
}
import os
import boto3
from botocore.exceptions import ClientError
def handler(message, context):
# Store the SECRETS_NAMESPACE value from the Function's environment variables
secretsNamespace = os.environ['SECRETS_NAMESPACE']
secretsRegion = os.environ['SECRETS_REGION']
# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=secretsRegion
)
try:
# SecretId is a combination of the secret's namespace and the specific secret to return
response = client.get_secret_value(SecretId=secretsNamespace + 'apiKey')
except ClientError as e:
# Handle specific errors (omitted)
else:
apiKey = response['SecretString']
import java.util.HashMap;
package aws.example.secretsmanager;
// ... For all required imports, see link above
public class Handler {
public static void main(String[] args) {
getSecret();
}
public static void getSecret() {
// Store the SECRETS_NAMESPACE, _ENDPOINT, and _REGION values from the Function's environment variables
String secretsName = System.getenv("SECRETS_NAMESPACE")
String endpoint = System.getenv("SECRETS_ENDPOINT")
String region = System.getenv("SECRETS_REGION")
AwsClientBuilder.EndpointConfiguration config = new AwsClientBuilder.EndpointConfiguration(endpoint, region);
AWSSecretsManagerClientBuilder clientBuilder = AWSSecretsManagerClientBuilder.standard();
clientBuilder.setEndpointConfiguration(config);
AWSSecretsManager client = clientBuilder.build();
String secret;
GetSecretValueRequest getSecretValueRequest = new GetSecretValueRequest().withSecretId(secretName + 'apiKey');
GetSecretValueResult getSecretValueResult = null;
try {
getSecretValueResult = client.getSecretValue(getSecretValueRequest);
} catch(ResourceNotFoundException e) {
// Handle specific errors (omitted)
}
if(getSecretValueResult == null) {
return;
}
// Verify SecretString exists, print
if(getSecretValueResult.getSecretString() != null) {
secret = getSecretValueResult.getSecretString();
System.out.println(secret);
}
}
}
using System;
using System.IO;
using Amazon;
using Amazon.Lambda.Core;
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
namespace StackeryFunction
{
public class Handler
{
[LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
public dynamic handler(dynamic message)
{
// Store the SECRETS_NAMESPACE AND _REGION value from the Function's environment variables
string secretName = Environment.GetEnvironmentVariable("SECRETS_NAMESPACE");
string region = Environment.GetEnvironmentVariable("SECRETS_REGION");
string secret = "";
MemoryStream memoryStream = new MemoryStream();
IAmazonSecretsManager client = new AmazonSecretsManagerClient(RegionEndpoint.GetBySystemName(region));
GetSecretValueRequest request = new GetSecretValueRequest();
request.SecretId = secretName;
request.VersionStage = "AWSCURRENT"; // VersionStage defaults to AWSCURRENT if unspecified.
GetSecretValueResponse response = null;
try
{
response = client.GetSecretValueAsync(request).Result;
}
catch { {**ExceptionName**} e } {
// Handle specific errors (omitted)
}
if (response.SecretString != null)
{
secret = response.SecretString;
}
}
}
}
Full AWS Code Examples: