logo

Database

Sensitive information in source code

Need

Removal of signing secrets from source code in favor of runtime configuration

Context

• Usage of Rust 1.75+ for building application services

• Usage of the jsonwebtoken crate for signing JSON Web Tokens

Description

1. Non compliant code

use jsonwebtoken::EncodingKey;

fn signing_key() -> EncodingKey {
    // The signing secret ships with the source and the compiled binary
    EncodingKey::from_secret(b"super-secret-key")
}

The `signing_key` function below builds the key that signs every JSON Web Token of the application from the literal `b"super-secret-key"`. A secret written in the source is not secret. It is visible to everyone with access to the repository, it stays in the version control history after it is removed, and it is embedded in plain text in the compiled binary, where `strings` recovers it. With that value, an attacker can sign tokens with any `sub` or role claim, and the application accepts them as genuine. A hardcoded secret also cannot be rotated. Changing it requires a new build and deployment, so after a leak the forged tokens keep working until the next release, and every environment that runs the same build shares the same key.

2. Steps

• Remove every literal passed to `EncodingKey::from_secret`, `DecodingKey::from_secret` and their base64 variants, and load the secret at runtime instead.

• Store the secret in a secrets manager and expose it to the service through an environment variable or a mounted file.

• Fail at startup when the secret is missing or shorter than 32 bytes instead of using a default value.

• Rotate the secret that was committed, since it remains in the repository history, and revoke the tokens signed with it.

• Prefer asymmetric algorithms such as `EdDSA` or `ES256` when several services only need to verify tokens, so the private key stays with the issuer.

• Add a secret scanner to the CI pipeline to block new hardcoded secrets.

3. Secure code example

use jsonwebtoken::EncodingKey;
use std::env;

const MIN_SECRET_LEN: usize = 32;

fn signing_key() -> Result<EncodingKey, String> {
    // The secret is injected at runtime and can be rotated without a new build
    let secret = env::var("JWT_SECRET").map_err(|_| "JWT_SECRET is not set".to_string())?;...

The corrected function reads the secret from the `JWT_SECRET` environment variable at runtime. The value is injected by the deployment platform, typically from a secrets manager such as AWS Secrets Manager or HashiCorp Vault, so it never appears in the repository or the binary and can be rotated without rebuilding. The function returns an error instead of falling back to a default when the variable is missing, and it rejects secrets shorter than 32 bytes, which is the minimum key length for HS256. Failing at startup is safer than silently signing tokens with an empty or weak key. The key should be built once when the service starts and shared with the handlers, for example through `actix_web::web::Data`, instead of reading the environment on every request.