Manage Secrets and Config in Scala Apps

Khimananda Oli 8 min read Programming and Languages
Manage Secrets and Config in Scala Apps

By Khimananda Oli | Last reviewed: August 2026

Hardcoded credentials and scattered property files remain the leading cause of security incidents in JVM applications. When you manage secrets and config in Scala apps, you must balance developer ergonomics with strict audit requirements like SOC 2 or ISO 27001. The solution is a layered approach that combines type-safe loading at startup with externalized secret injection at runtime. This guide covers the exact patterns I use to keep Scala services secure and observable, building on principles from our CI/CD secrets handling guide.

Configuration Precedence Stack1. Runtime Secrets (Vault / Env Vars)2. Environment Overrides (APP_DB_HOST)3. Environment-Specific (application-prod.conf)4. Default Reference (reference.conf)Highest PriorityLowest Priority
Figure 1: Configuration precedence stack when you manage secrets and config in Scala apps using PureConfig and HOCON.

How do you implement type-safe configuration in Scala?

The most common mistake teams make is treating configuration as unstructured string maps. In production Scala, you should treat configuration as a strongly-typed data structure that fails fast if validation errors occur. PureConfig has become the industry standard for this because it derives readers directly from your case classes without boilerplate.

Defining the Configuration Schema

Your configuration schema should mirror your domain model. Avoid nested Maps or generic JSON objects. Instead, define explicit case classes that represent every tunable parameter in your system. This makes refactoring safe and provides IDE autocomplete for operators managing deployments.

// src/main/scala/com/example/config/AppConfig.scala
package com.example.config

import scala.concurrent.duration.FiniteDuration
import pureconfig._
import pureconfig.generic.auto._

case class DatabaseConfig(
  host: String,
  port: Int,
  name: String,
  username: String,
  password: String, // Injected via env var, never in reference.conf
  maxPoolSize: Int,
  connectionTimeout: FiniteDuration
)

case class ServerConfig(
  host: String,
  port: Int,
  gracefulShutdownTimeout: FiniteDuration
)

case class AppConfig(
  server: ServerConfig,
  database: DatabaseConfig,
  featureFlags: Map[String, Boolean]
)

Loading and Validating at Startup

Load configuration once during application initialization. If any required field is missing or has an invalid type, the application should terminate immediately with a descriptive error message. This "fail-fast" behavior prevents partially-initialized services from accepting traffic in a broken state.

import pureconfig._
import pureconfig.module.catseffect.syntax._
import cats.effect.IO

object ConfigLoader {
  def load: IO[AppConfig] = {
    ConfigSource.default
      .loadF[IO, AppConfig]()
      .handleErrorWith { err =>
        IO.raiseError(new RuntimeException(
          s"Failed to load configuration: ${err.prettyPrint()}"
        ))
      }
  }
}

This pattern ensures that your entire dependency graph is validated before a single HTTP request is served. For teams managing complex microservices, this level of rigor is essential for maintaining reliability across environments.

How do you securely inject secrets into Scala applications?

Secrets are distinct from configuration. While configuration defines how the app behaves, secrets define access. You should never store secrets in HOCON files, even if those files are gitignored. Instead, use environment variable substitution or direct integration with a secrets manager. For deeper infrastructure context, see our article on Kubernetes secrets management done right.

Environment Variable Substitution in HOCON

HOCON supports native environment variable substitution with fallback syntax. This allows your reference.conf to remain safe for version control while still being overridden in production.

# reference.conf
database {
  host = "localhost"
  port = 5432
  name = "app_dev"
  
  # Required secret - app fails to start if DB_PASSWORD is unset
  password = ${?DB_PASSWORD}
  
  # Optional override with safe default
  username = ${?DB_USERNAME}
  username = "app_user"
}

The ${?VAR} syntax returns null if the variable is absent, allowing PureConfig to report a clear validation error rather than a cryptic NullPointerException. Always prefer this over hardcoded defaults for sensitive fields.

Integrating HashiCorp Vault

For SOC 2 compliance or multi-tenant systems, environment variables alone may not suffice. HashiCorp Vault provides dynamic secrets, leasing, and audit trails. In Scala, use the official Vault Java driver wrapped in a functional effect.

import io.github.jopenlibs.vault.Vault
import io.github.jopenlibs.vault.response.LogicalResponse
import cats.effect.IO

object VaultClient {
  def getDbCredentials(path: String): IO[(String, String)] = IO.blocking {
    val response: LogicalResponse = Vault.config()
      .address(sys.env("VAULT_ADDR"))
      .token(sys.env("VAULT_TOKEN"))
      .build()
      .logical()
      .read(path)
    
    val data = response.getData
    (data.get("username"), data.get("password"))
  }.adaptError { case e => 
    new RuntimeException(s"Vault fetch failed for $path", e)
  }
}

Fetch these credentials during the same startup phase as your PureConfig load, then merge them into your final AppConfig instance. This keeps your business logic completely unaware of where secrets originate.

Startup Secret Injection FlowHashiCorp VaultDynamic CredentialsEnvironment VarsK8s Secrets / CIPureConfig LoaderMerge & ValidateImmutable AppConfigType-Safe Runtime
Figure 2: Secret injection flow showing how Vault and environment variables merge into a validated AppConfig during Scala app startup.

What are the best practices for structuring HOCON files?

HOCON's flexibility can become a liability if not constrained by convention. After auditing dozens of Scala codebases, I've found that consistent structure reduces onboarding time and deployment errors significantly.

  • Separate reference.conf from application.conf: reference.conf ships with your JAR and contains safe defaults. application.conf is environment-specific and often generated or mounted at deploy time.
  • Use includes sparingly: Deep include chains make debugging configuration resolution nearly impossible. Prefer flat structures with clear namespacing.
  • Document every key: Use HOCON comments (#) above each field explaining its purpose, valid range, and whether it requires a restart.
  • Avoid path-based overrides in production: Don't rely on -Dconfig.file pointing to different files per environment. Instead, use the same file structure with environment variable overrides.
  • Namespace by component: Group related settings under logical prefixes like http.client, kafka.consumer, or metrics.exporter rather than dumping everything at root level.

These conventions align well with observability practices. When your configuration keys map cleanly to metric labels or log contexts, debugging becomes significantly faster. See our structured logging best practices guide for complementary patterns.

How does PureConfig compare to other Scala config libraries?

Choosing the right library depends on your team's size, compliance requirements, and tolerance for boilerplate. Here's a practical comparison based on 2026 ecosystem maturity.

LibraryType SafetySecret SupportLearning CurveBest For
PureConfigFull (case class derivation)Via env/Vault integrationModerateProduction services, SOC 2
CirisFull (effectful loading)Native (Vault, AWS SSM, etc.)HighFunctional stacks (cats-effect)
Typesafe ConfigNone (manual getters)Manual onlyLowLegacy Akka/Lagom apps
ZIO ConfigFull (ZIO-native)Native providersModerateZIO-based applications

For most teams starting new projects in 2026, PureConfig offers the best balance of safety and pragmatism. Ciris is superior if you're already deep in the cats-effect ecosystem and need first-class secret provider support without custom glue code. Avoid raw Typesafe Config unless maintaining legacy systems; the lack of compile-time safety leads to runtime failures that are expensive to diagnose in distributed environments.

Scala Config Library Decision TreeStart HereUsing ZIO Effect System?YesNoZIO ConfigCats-Effect Native?YesNoCirisPureConfigAll options support env vars & Vault integration
Figure 3: Decision tree for selecting the right configuration library when you manage secrets and config in Scala apps.

How do you test configuration loading without exposing secrets?

Testing configuration is often neglected because developers fear leaking real credentials into test fixtures. The solution is to separate schema validation from value verification.

  1. Create test-only reference files: Place test/resources/application-test.conf with dummy but structurally valid values. Never copy production secrets here.
  2. Test failure modes explicitly: Write tests that assert specific error messages when required keys are missing. This validates your fail-fast behavior.
  3. Use PureConfig's ConfigReader tests: Unit test custom readers independently of file I/O using in-memory Config objects.
  4. Mock secret providers: When testing Vault integration, use a fake client that returns predictable values. Never call real Vault in unit tests.
  5. Validate redaction logic: Ensure your logging and error reporting never accidentally print secret values. Add explicit tests for toString implementations.
// Example: Testing missing required secret
class ConfigSpec extends AnyFlatSpec with Matchers {
  it should "fail with clear message when DB_PASSWORD is missing" in {
    val source = ConfigSource.string(
      """database { host = "localhost", port = 5432, name = "test" }"""
    )
    
    val result = source.load[AppConfig]
    result.isLeft shouldBe true
    result.left.get.prettyPrint() should include("database.password")
  }
}

This testing discipline ensures your configuration layer remains reliable as your system evolves. It also satisfies auditors who require evidence that secret handling is verified programmatically, not just procedurally.

Secure Configuration Is a Continuous Practice

When you manage secrets and config in Scala apps correctly, you build a foundation that scales from local development to regulated production environments. Start with PureConfig and environment variables, graduate to Vault when compliance demands it, and always validate eagerly at startup. Review your configuration strategy quarterly as part of your broader security posture assessment. If your team needs help implementing these patterns or preparing for an audit, reach out to discuss your specific requirements.

Frequently Asked Questions

Typesafe Config (HOCON) remains the default choice in 2026. It supports environment variable substitution, system properties, and file overrides natively. Most frameworks like Pekko and Play integrate it directly without extra dependencies.

Use environment variables or mounted secret files at runtime. Tools like HashiCorp Vault or AWS Secrets Manager provide dynamic injection. Never store plaintext credentials in application.conf; use reference keys that resolve externally during deployment.

Yes. PureConfig 0.18+ derives codecs automatically from case classes, eliminating manual parsing. It wraps Typesafe Config but adds compile-time validation and better error reporting for missing or malformed fields in your Scala configuration.

Use HOCON’s include mechanism with env-specific override files like application-prod.conf. Load order ensures production values override defaults. Combine this with CI/CD variable injection to avoid maintaining separate full config files per environment.

Integrate a secrets manager SDK such as Vault4s or aws-sdk-scala. Fetch encrypted values on startup and cache them in memory. Avoid decryption logic in config files; keep it in initialization code with proper error handling and fallbacks.

Yes. HOCON offers superior substitution, comments, and includes compared to YAML. The Scala ecosystem maintains strong tooling support for it, and most libraries expect HOCON format natively, reducing integration friction and parsing errors.

Fail fast by loading and validating all required config keys during main method initialization. Use PureConfig or custom validators to check types, ranges, and presence. Throw descriptive exceptions immediately rather than encountering runtime failures later.

Yes. Mount Kubernetes secrets as volume files or environment variables. Configure Typesafe Config to read from /etc/secrets or map env vars using ${?SECRET_NAME}. No special SDK needed if your deployment manifests handle the mounting correctly.

Avoid baking secrets into images via COPY or ENV in Dockerfiles. Use runtime injection through orchestrators or secret managers. Also ensure .dockerignore excludes local secret files and that multi-stage builds do not leak intermediate layers containing credentials.

Implement periodic polling against your secrets backend using a scheduled task or reactive stream. Cache refreshed values atomically. Note that some resources like database pools may require reconnection logic to pick up new credentials safely.

Ciris 3.x provides effectful, composable config decoding for ZIO, Cats Effect, and FS2. It enforces explicit sourcing and validation, making it ideal for functional Scala apps where side-effect isolation and testability matter more than convenience.

Provide test-specific application-test.conf with dummy values. Override config sources in tests using System.setProperty or PureConfig’s ConfigSource. Never connect to production backends; mock external secret providers or use embedded test containers for integration tests.

Apply least privilege: grant read-only access to specific secret paths only. Rotate credentials regularly and audit access logs. In cloud environments, bind IAM roles to pods or VMs instead of distributing long-lived API keys to applications.

Yes. Reflection-based config parsing requires explicit hints. Register Typesafe Config or PureConfig classes in reflect-config.json. Test native builds thoroughly since missing registrations cause silent failures or runtime crashes during config initialization in 2026.

Convert key=value pairs to HOCON syntax incrementally. Typesafe Config reads both formats simultaneously during transition. Validate equivalence with automated tests comparing parsed outputs. Remove .properties files only after confirming all environments load HOCON correctly.