Skip to Content
Course content

172: Handling Secrets in Scala Applications

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started working on production Scala services, I did what most people do: I put my database passwords and API keys in an application.conf file using environment variable substitutions like db.password = ${?DB_PASSWORD}. I figured that as long as the actual password lived in the environment and wasn't committed to Git, I was "doing security."

The Myth: Environment Variables are a Secure Vault

Here is why that thinking is dangerous. Environment variables are not a vault; they are just a different place to store a plaintext string. If you have a vulnerability that allows an attacker to dump process environment variables, or if you're using a CI/CD tool that logs the environment during a failed build, your "secret" is now public. Worse, in the JVM, once that environment variable is read into a String, it's floating around in the heap. Because of how the JVM handles strings, that password could linger in memory long after you've finished connecting to the database, waiting for a heap dump to reveal it.

The Reality: Secrets Require Managed Access and Memory Guardrails

To actually handle secrets professionally, you need to move away from "ambient" configuration and toward a "pull" model using a dedicated Secret Manager (like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault). Instead of the app inheriting secrets at startup, the app should authenticate as a service identity and request the secret it needs at runtime.

But fetching the secret is only half the battle. How you hold it in Scala matters. If you store a secret in a standard String, you're asking for trouble. I've seen far too many "debug" logs where someone printed a configuration object to the console, accidentally leaking the production API key into the ELK stack for all to see.

A better pattern is to wrap your secrets in a dedicated type that prevents accidental leakage. Here is how I typically structure a basic secret wrapper to avoid the toString trap:

case class Secret(value: String) {
  override def toString: String = "[REDACTED]"
  
  // We explicitly name this 'reveal' to make the developer 
  // consciously decide to access the raw value.
  def reveal: String = value
}

// Usage
val apiKey = Secret("sk_test_51MzX...")
println(s"Connecting with key: $apiKey") 
// Output: Connecting with key: [REDACTED]

By overriding toString, you stop the most common leak vector: the developer's own curiosity during a debugging session. I'll admit, this doesn't solve the heap dump problem (for that, you'd need Array[Char] and manual clearing, which is often overkill for standard business apps), but it solves the "log leak" problem, which is where 90% of secrets actually escape.

When integrating with a real manager, your code should look less like "reading a config file" and more like an effectful call. If you're using ZIO or Cats Effect, this is usually a service call that happens during the initialization phase of your application, ensuring that the secret is fetched once and passed into the dependencies that actually need it, rather than being globally accessible via a config object.




📋 Practical Task

Implementing a Log-Proof Secret Provider

You are tasked with hardening a legacy Scala module that currently handles an ExternalApiConfig object containing a plaintext API key. Your goal is to prevent this key from ever appearing in logs, even if the entire config object is printed.

Requirements:

  • Create a Secret value class or case class that overrides toString to return "***".
  • Create a SecretProvider trait with a method fetchKey(keyName: String): Secret.
  • Implement a MockSecretProvider that simulates a network call to a vault and returns a Secret containing the value "super-secret-token-123".
  • Write a small main method that:
    1. Fetches the secret using the provider.
    2. Attempts to print the secret directly using string interpolation (which should result in "***").
    3. Calls a simulated makeApiCall(token: String) method by explicitly extracting the value from the Secret wrapper.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.