Deepen PracticeOrdered learning track

Secret Injection Patterns

Learn Java Microservices File Handling, State, Configuration and Secret Management - Part 047

Pattern injeksi secret ke Java microservices melalui environment variable, mounted file, sidecar, init container, API fetch, service mesh, dan external secret operator beserta trade-off lifecycle, leakage, reload, audit, dan failure mode.

16 min read3073 words
PrevNext
Lesson 4770 lesson track39–58 Deepen Practice
#java#microservices#secrets#kubernetes+3 more

Part 047 — Secret Injection Patterns

Secret injection is not a syntax choice.

It is a runtime trust-boundary decision.

Setelah memahami realita Kubernetes Secret, kita perlu menjawab pertanyaan yang lebih praktis:

Bagaimana secret sampai ke Java process?

Jawaban populer biasanya terlalu cepat:

Pakai environment variable saja.
Mount sebagai file saja.
Ambil dari Vault saja.

Jawaban seperti itu tidak cukup untuk production. Cara secret diinjeksi menentukan:

  • kapan secret tersedia;
  • siapa yang bisa membacanya;
  • apakah secret bisa dirotasi tanpa restart;
  • apakah secret berisiko bocor ke process list, logs, crash dump, atau actuator;
  • apakah service bisa survive secret manager outage;
  • apakah startup fail fast atau degrade;
  • apakah audit trail ada;
  • apakah secret punya TTL/lease yang dihormati;
  • apakah runtime bisa membedakan secret lama dan baru;
  • apakah deployment GitOps tetap bersih dari plaintext secret.

Di part ini kita akan membahas pattern injeksi secret yang paling sering dipakai dalam Java microservices:

  1. Environment variable injection.
  2. Mounted Secret volume.
  3. Config tree / file-based binding.
  4. Init container materialization.
  5. Sidecar/agent template rendering.
  6. Runtime API fetch.
  7. External Secrets Operator / controller sync.
  8. CSI Secret Store driver.
  9. Service mesh / workload identity based secretless access.
  10. Hybrid pattern.

Tujuannya bukan memilih satu pattern yang selalu benar. Tujuannya adalah membangun decision framework.


1. Secret Delivery Plane

Sebelum bicara pattern, pisahkan empat plane berikut.

PlaneContohPertanyaan Utama
AuthorityVault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, PKISiapa menerbitkan secret?
DeliveryKubernetes Secret, mounted file, sidecar, CSI, API callBagaimana secret sampai ke workload?
ConsumptionJava config binding, HikariCP, HTTP client, SDK credential providerBagaimana secret dipakai?
TargetDatabase, broker, API, object storeSecret memberi capability apa?

Kesalahan umum adalah menganggap delivery plane sebagai authority.

Contoh:

Kubernetes Secret bukan selalu source of truth.
Ia sering hanya cache/delivery object dari Vault atau cloud secret manager.

Jika authority ada di Vault tetapi aplikasi membaca Kubernetes Secret, maka rotation semantics tergantung pada dua hal:

  1. bagaimana Vault secret berubah;
  2. bagaimana perubahan itu disinkronkan ke Kubernetes Secret;
  3. bagaimana Pod menerima update;
  4. bagaimana Java process reload/reconnect.

2. Decision Dimensions

Setiap injection pattern harus dievaluasi dengan dimensi berikut.

DimensionPertanyaan
Startup availabilityApakah secret harus tersedia sebelum app start?
Runtime refreshApakah secret bisa berubah tanpa restart?
TTL/lease awarenessApakah secret punya expiry yang harus dihormati?
Blast radiusJika bocor, capability-nya sebesar apa?
Exposure surfaceSecret terlihat di env, file, memory, logs, API, process metadata?
Operational couplingApakah app tergantung langsung ke secret manager saat startup/runtime?
AuditabilityApakah secret access dicatat di authority?
Language couplingApakah app perlu library khusus?
Kubernetes couplingApakah pattern hanya cocok di Kubernetes?
GitOps compatibilityApakah plaintext secret bisa dihindari dari Git?
Failure modeFail closed, stale credential, crash loop, degraded mode?

Jangan pilih pattern sebelum menjawab ini.


3. Pattern 1 — Environment Variable Injection

Pattern paling umum:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: evidence-service
spec:
  template:
    spec:
      containers:
        - name: app
          image: registry.example.com/evidence-service:1.0.0
          env:
            - name: DB_USERNAME
              valueFrom:
                secretKeyRef:
                  name: evidence-db-secret
                  key: username
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: evidence-db-secret
                  key: password

Java consumption:

spring:
  datasource:
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

3.1 Strengths

Environment variable injection kuat karena sederhana:

  • mudah dipahami;
  • bekerja dengan Spring Boot externalized configuration;
  • cocok untuk static secret yang jarang berubah;
  • tidak perlu application code khusus;
  • mudah digunakan oleh legacy library;
  • fail fast saat secret missing jika config validation benar.

Spring Boot memang mendukung environment variable sebagai salah satu source externalized configuration.

3.2 Weaknesses

Kelemahan utama:

Environment variable is a startup-time snapshot.

Jika Secret Kubernetes berubah, environment variable di process yang sudah berjalan tidak berubah.

Risiko lain:

  • secret bisa muncul di diagnostic dump;
  • secret bisa terlihat oleh process introspection tertentu;
  • secret mudah ikut tercetak saat developer dump environment;
  • rotation biasanya butuh restart/rollout;
  • tidak cocok untuk short-lived dynamic secret;
  • sulit membedakan secret version di runtime.

3.3 Good Use Cases

Cocok untuk:

  • static-ish secret dengan rotation terjadwal melalui rollout;
  • secret bootstrap low-frequency;
  • non-lease secret;
  • credential yang target client-nya tidak mendukung reload;
  • deployment kecil dengan operational maturity terbatas.

Tidak cocok untuk:

  • Vault dynamic database credentials dengan TTL pendek;
  • certificate/key yang harus rotate tanpa downtime;
  • high-security workloads yang ingin minimize env exposure;
  • multi-tenant secret yang berubah per request.

3.4 Production Guardrails

Gunakan guardrail berikut:

@ConfigurationProperties(prefix = "app.database")
@Validated
public record DatabaseSecretProperties(
    @NotBlank String username,
    @NotBlank String password
) {}

Mapping environment:

app:
  database:
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

Tambahkan startup check:

@Component
final class SecretStartupCheck implements ApplicationRunner {
    private final DatabaseSecretProperties properties;

    SecretStartupCheck(DatabaseSecretProperties properties) {
        this.properties = properties;
    }

    @Override
    public void run(ApplicationArguments args) {
        if (properties.password().length() < 16) {
            throw new IllegalStateException("Database password appears invalid");
        }
    }
}

Jangan log:

log.info("Database config: username={}, password={}", username, password); // never

Lebih aman:

log.info("Database config loaded: usernamePresent={}, passwordPresent={}",
    username != null && !username.isBlank(),
    password != null && !password.isBlank());

4. Pattern 2 — Mounted Secret Volume

Kubernetes Secret bisa diproyeksikan sebagai file.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: evidence-service
spec:
  template:
    spec:
      containers:
        - name: app
          image: registry.example.com/evidence-service:1.0.0
          volumeMounts:
            - name: db-secret
              mountPath: /etc/secrets/db
              readOnly: true
      volumes:
        - name: db-secret
          secret:
            secretName: evidence-db-secret

File di container:

/etc/secrets/db/username
/etc/secrets/db/password

4.1 Java Read Pattern

public final class FileSecretReader {
    public String readSecret(Path path) {
        try {
            return Files.readString(path, StandardCharsets.UTF_8).trim();
        } catch (IOException e) {
            throw new IllegalStateException("Failed to read secret file: " + path, e);
        }
    }
}

Untuk secret besar/cert/key:

public byte[] readSecretBytes(Path path) {
    try {
        return Files.readAllBytes(path);
    } catch (IOException e) {
        throw new IllegalStateException("Failed to read secret file: " + path, e);
    }
}

4.2 Strengths

Mounted file pattern lebih baik dari env untuk beberapa kasus:

  • secret tidak masuk environment variable;
  • cocok untuk TLS cert, private key, truststore;
  • bisa dibaca ulang dari file;
  • Kubernetes dapat memperbarui mounted Secret volume secara eventually consistent;
  • lebih natural untuk library yang menerima file path.

4.3 Weaknesses

Kelemahannya:

  • update tidak instan;
  • subPath mount tidak menerima automated Secret update;
  • Java code harus tahu kapan file berubah;
  • library mungkin hanya baca file saat startup;
  • file permission perlu diperhatikan;
  • mounted Secret tetap tersedia untuk process dalam container;
  • restart tetap sering lebih sederhana untuk banyak client.

4.4 File Watcher Pattern

Java bisa menonton directory dengan WatchService, tetapi jangan overestimate reliabilitas watcher lintas filesystem/projection.

Pattern lebih aman:

periodic poll + content hash + atomic swap in application state

Contoh:

public final class PollingSecretFileProvider {
    private final Path path;
    private volatile SecretSnapshot current;

    public PollingSecretFileProvider(Path path) {
        this.path = path;
        this.current = readSnapshot();
    }

    public SecretSnapshot current() {
        return current;
    }

    @Scheduled(fixedDelayString = "${secrets.file.poll-interval:30s}")
    public void refresh() {
        SecretSnapshot next = readSnapshot();
        if (!next.hash().equals(current.hash())) {
            current = next;
        }
    }

    private SecretSnapshot readSnapshot() {
        try {
            String value = Files.readString(path, StandardCharsets.UTF_8).trim();
            String hash = sha256(value);
            return new SecretSnapshot(value, hash, Instant.now());
        } catch (IOException e) {
            throw new IllegalStateException("Failed to read secret file", e);
        }
    }

    private static String sha256(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }
}

public record SecretSnapshot(String value, String hash, Instant loadedAt) {}

Tetapi ingat: membaca secret baru tidak otomatis membuat semua downstream client memakai secret baru. HikariCP, Kafka client, HTTP client, SDK credential provider, TLS context, dan connection pool punya lifecycle sendiri.


5. Pattern 3 — Spring Boot Config Tree

Spring Boot mendukung import directory tree sebagai configuration source dengan configtree:. Pattern ini cocok untuk mounted Kubernetes Secret/ConfigMap sebagai file.

spring:
  config:
    import: optional:configtree:/etc/secrets/db/

Jika directory berisi:

/etc/secrets/db/username
/etc/secrets/db/password

Maka property bisa dibaca sebagai:

app:
  database:
    username: ${username}
    password: ${password}

Atau gunakan prefix directory structure yang lebih eksplisit.

5.1 Strengths

  • cocok dengan Kubernetes mounted Secret;
  • lebih bersih daripada manual Files.readString;
  • bisa dipakai dengan @ConfigurationProperties;
  • startup validation tetap bekerja;
  • tidak perlu env var untuk secret.

5.2 Weaknesses

  • tetap mostly startup-bound;
  • dynamic reload tidak otomatis menyelesaikan client reconnect;
  • property name bisa bentrok jika directory tidak diatur;
  • secret masih masuk Spring Environment, sehingga perlu hati-hati dengan actuator/env exposure.

Gunakan directory dengan namespace jelas:

/etc/app-secrets/
  datasource/
    username
    password
  kafka/
    sasl-username
    sasl-password
  tls/
    client.crt
    client.key

Config:

spring:
  config:
    import:
      - optional:configtree:/etc/app-secrets/datasource/
      - optional:configtree:/etc/app-secrets/kafka/

Untuk production, jangan expose /actuator/env bebas. Jika actuator dipakai, sanitization dan authorization wajib.


6. Pattern 4 — Init Container Materialization

Init container mengambil secret dari authority lalu menulis ke shared volume sebelum app container start.

6.1 Example Shape

apiVersion: apps/v1
kind: Deployment
metadata:
  name: evidence-service
spec:
  template:
    spec:
      volumes:
        - name: rendered-secrets
          emptyDir:
            medium: Memory
      initContainers:
        - name: fetch-secrets
          image: secret-fetcher:1.0.0
          volumeMounts:
            - name: rendered-secrets
              mountPath: /rendered-secrets
      containers:
        - name: app
          image: evidence-service:1.0.0
          volumeMounts:
            - name: rendered-secrets
              mountPath: /etc/secrets
              readOnly: true

6.2 Strengths

  • app tidak butuh library Vault/cloud secret manager;
  • secret tidak perlu disimpan sebagai Kubernetes Secret;
  • startup fail-fast mudah;
  • cocok untuk bootstrap material seperti truststore atau config template;
  • secret bisa ditulis ke memory-backed emptyDir.

6.3 Weaknesses

  • no runtime refresh unless pod restart;
  • init container harus punya identity/permission ke secret authority;
  • secret material ada di shared volume;
  • jika secret expired setelah start, app tidak tahu;
  • cocok untuk static secret, kurang cocok untuk dynamic lease pendek.

6.4 Use Case

Cocok untuk:

  • certificate bundle saat startup;
  • legacy app yang hanya bisa baca file;
  • static credential dengan rollout-based rotation;
  • rendering config file dari template.

Tidak cocok untuk:

  • dynamic DB credential TTL 15 menit;
  • per-request credential;
  • high-frequency rotation.

7. Pattern 5 — Sidecar / Agent Rendering

Sidecar atau agent berjalan bersama app, mengambil secret dari authority, lalu merender file ke shared volume. Contoh umum: Vault Agent injector/template.

7.1 Strengths

  • app tidak perlu langsung bicara ke Vault;
  • agent bisa handle auth/renewal/template rendering;
  • secret bisa diupdate sebagai file;
  • identity/lease logic dipindahkan ke sidecar;
  • cocok untuk polyglot environment.

7.2 Weaknesses

  • Java app tetap perlu reload/reconnect;
  • sidecar menambah resource dan operational surface;
  • template rendering failure harus dimonitor;
  • shared volume permission harus benar;
  • dependency startup ordering harus jelas;
  • jika agent crash, secret refresh bisa berhenti sementara app masih hidup.

7.3 Reload Coordination

Jangan berhenti di “agent menulis file baru”. Pertanyaan penting:

Siapa yang memberitahu Java process untuk membangun ulang client?

Opsi:

  1. Java app polling file hash.
  2. Sidecar mengirim HTTP reload hook ke localhost.
  3. App restart saat secret berubah.
  4. Library client mengambil credential dari provider yang membaca file setiap kali dibutuhkan.
  5. Connection pool punya max lifetime sehingga credential lama hilang bertahap.

7.4 Safe File Swap

Agent sebaiknya menulis secret secara atomic:

write secret.tmp
fsync if required
rename secret.tmp -> secret

Reader Java sebaiknya membaca snapshot utuh, bukan file yang sedang ditulis.


8. Pattern 6 — Runtime API Fetch

Java app langsung mengambil secret dari authority menggunakan client library/API.

8.1 Strengths

  • app bisa lease-aware;
  • dynamic secret lebih natural;
  • audit access tercatat langsung di authority;
  • refresh bisa dikontrol oleh app;
  • per-tenant/per-operation credential mungkin dilakukan;
  • tidak perlu secret disimpan di Kubernetes Secret.

8.2 Weaknesses

  • app code/library coupling ke secret provider;
  • startup tergantung availability secret manager;
  • runtime outage secret manager bisa berdampak ke app;
  • retry/backoff harus hati-hati agar tidak DDoS secret manager;
  • secret caching harus benar;
  • developer harus memahami TTL/renew/revoke.

8.3 Java Abstraction

Jangan sebar client Vault/AWS/Azure/GCP di seluruh codebase.

Buat abstraction:

public interface SecretProvider {
    SecretMaterial get(String logicalName);
}

public record SecretMaterial(
    String logicalName,
    Map<String, String> values,
    Instant loadedAt,
    Optional<Instant> expiresAt,
    Optional<String> version,
    Optional<String> leaseId
) {
    public String required(String key) {
        String value = values.get(key);
        if (value == null || value.isBlank()) {
            throw new IllegalStateException("Missing secret key: " + key);
        }
        return value;
    }
}

Business code tidak perlu tahu apakah secret berasal dari Vault, AWS Secrets Manager, mounted file, atau Kubernetes Secret.

8.4 Cache Policy

Runtime fetch tidak berarti fetch setiap request.

Gunakan cache berbasis TTL:

public final class CachingSecretProvider implements SecretProvider {
    private final SecretProvider delegate;
    private final Duration maxCacheTtl;
    private final ConcurrentHashMap<String, SecretMaterial> cache = new ConcurrentHashMap<>();

    public CachingSecretProvider(SecretProvider delegate, Duration maxCacheTtl) {
        this.delegate = delegate;
        this.maxCacheTtl = maxCacheTtl;
    }

    @Override
    public SecretMaterial get(String logicalName) {
        SecretMaterial current = cache.get(logicalName);
        if (current != null && !shouldRefresh(current)) {
            return current;
        }
        SecretMaterial next = delegate.get(logicalName);
        cache.put(logicalName, next);
        return next;
    }

    private boolean shouldRefresh(SecretMaterial material) {
        Instant now = Instant.now();
        if (material.expiresAt().isPresent()) {
            Instant refreshBefore = material.expiresAt().get().minusSeconds(60);
            return now.isAfter(refreshBefore);
        }
        return material.loadedAt().plus(maxCacheTtl).isBefore(now);
    }
}

Production version harus menambahkan:

  • jitter;
  • single-flight refresh;
  • metrics;
  • fallback policy;
  • circuit breaker;
  • stale-if-error limit;
  • explicit failure mode.

9. Pattern 7 — External Secrets Operator / Controller Sync

External Secrets Operator pattern:

External Secret Store -> Kubernetes Secret -> Pod env/volume -> Java app

Controller membaca secret dari provider eksternal dan menulis Kubernetes Secret.

9.1 Strengths

  • app tidak perlu cloud/Vault SDK;
  • secret source of truth tetap eksternal;
  • GitOps bisa menyimpan ExternalSecret spec tanpa plaintext;
  • Kubernetes-native consumption;
  • multi-provider abstraction.

9.2 Weaknesses

  • Kubernetes Secret tetap menjadi materialized copy;
  • delay antara external secret update dan Pod consumption;
  • rotation butuh chain lengkap: provider -> controller -> K8s Secret -> Pod -> Java reload/restart;
  • controller failure bisa membuat secret stale;
  • RBAC ke Kubernetes Secret tetap kritikal.

9.3 Production Questions

  • Berapa refresh interval?
  • Apa yang terjadi jika provider unavailable?
  • Apakah controller menulis status condition?
  • Apakah alert jika sync gagal?
  • Apakah Kubernetes Secret immutable atau mutable?
  • Apakah Pod restart otomatis saat Secret berubah?
  • Apakah Java process membaca ulang?
  • Apakah Secret version tercatat di app metrics?

10. Pattern 8 — CSI Secret Store Driver

CSI Secret Store driver memungkinkan secret dari provider eksternal dimount sebagai volume ke Pod.

Mental model:

External provider -> CSI driver -> mounted file in Pod

10.1 Strengths

  • secret bisa tidak disimpan sebagai Kubernetes Secret, tergantung konfigurasi;
  • cocok untuk cert/key/file secret;
  • app consumption via file path;
  • provider-specific identity integration.

10.2 Weaknesses

  • app tetap perlu reload file;
  • provider/driver lifecycle menjadi dependency;
  • troubleshooting lebih kompleks;
  • rotation semantics harus dipahami per provider/driver;
  • tidak semua client library nyaman dengan changing file.

10.3 Best Fit

Cocok untuk:

  • TLS certificate injection;
  • cloud secret manager mounted as files;
  • app yang bisa reload file;
  • menghindari Kubernetes Secret materialization.

Kurang cocok untuk:

  • app yang hanya bisa baca env var;
  • high-frequency per-request secret;
  • credential yang harus dipakai oleh connection pool tanpa reload mechanism.

11. Pattern 9 — Secretless / Workload Identity

Pattern terbaik kadang bukan mengirim secret ke app, tetapi menghapus kebutuhan secret statis.

Contoh:

  • Kubernetes ServiceAccount + cloud workload identity untuk akses object storage;
  • IAM role for service account;
  • mTLS workload certificate issued automatically;
  • database auth berbasis workload identity;
  • service mesh identity untuk service-to-service auth.

Mental model:

Workload identity proves who the service is.
Target system grants capability based on identity.
No long-lived static password injected into app.

11.1 Strengths

  • mengurangi static secret;
  • rotation sering ditangani platform;
  • audit berbasis identity lebih jelas;
  • blast radius lebih kecil jika credential short-lived;
  • cocok untuk cloud-native workloads.

11.2 Weaknesses

  • platform coupling tinggi;
  • setup IAM/RBAC lebih kompleks;
  • local development perlu strategy;
  • tidak semua target mendukung workload identity;
  • debugging auth failure bisa lebih sulit.

11.3 Java Example: SDK Credential Provider

Untuk cloud SDK modern, gunakan default credential provider chain yang mengambil identity dari runtime environment, bukan hard-coded key.

Pseudo-pattern:

S3Client client = S3Client.builder()
    .region(Region.of(region))
    .credentialsProvider(DefaultCredentialsProvider.create())
    .build();

Jangan inject access key/secret key jika workload identity tersedia.


12. Hybrid Pattern

Production sering memakai hybrid:

Secret TypePattern
DB dynamic credentialVault/Spring Cloud Vault runtime integration or agent + reload
TLS certCSI or sidecar file rendering
API token staticExternal Secrets Operator -> K8s Secret -> mounted file/env
Cloud object storageworkload identity, no static secret
Bootstrap trust bundleinit container or mounted ConfigMap/Secret
Feature flag SDK keyKubernetes Secret env with rollout rotation

Tidak ada kewajiban memakai satu pattern untuk semua secret.

Yang penting adalah konsistensi decision record:

## Secret Injection ADR

### Secret
- Logical name:
- Authority:
- Consumer:
- Target resource:
- Sensitivity:

### Delivery Pattern
- Pattern:
- Why:
- Alternatives considered:

### Lifecycle
- Rotation frequency:
- TTL/lease:
- Reload strategy:
- Rollback strategy:

### Failure Mode
- Startup missing:
- Runtime refresh failure:
- Secret manager outage:
- Stale credential limit:

### Observability
- Metrics:
- Logs:
- Audit:
- Alert:

13. Injection Pattern Comparison

PatternRuntime RefreshApp CouplingExposureBest ForMain Risk
Env varNoLowMediumsimple static secretsrestart required
Mounted Secret volumePossible file updateLow/MediumMediumcerts, file secretsapp may not reload
Config treeMostly startupLowMediumSpring Boot file secretssecret in Environment
Init containerNoLowLow/Mediumbootstrap materialno refresh
Sidecar/agentYes-ishLow/MediumLow/MediumVault templates, certsreload coordination
Runtime API fetchYesHighLower materializationdynamic secretsprovider coupling
External Secrets OperatorDependsLowMediumGitOps external sourcestale sync chain
CSI driverDependsLow/MediumLow/Mediumfile/cert secretdriver complexity
Workload identityPlatform-managedMediumLowcloud/service authplatform coupling

14. Secret Reload Is Not One Operation

Reload chain:

Jika salah satu stage tidak ada, rotation tanpa restart bisa gagal.

Contoh umum:

Secret file changed.
Spring property changed.
But HikariCP still uses old connections.

Atau:

Vault issued new DB credential.
App loaded it.
Old pool connections still alive.
Old credential revoked.
Requests fail.

Untuk database credentials, pertimbangkan:

  • Hikari maxLifetime lebih pendek dari credential TTL;
  • controlled pool restart;
  • dual credential overlap;
  • readiness fail jika refresh gagal mendekati expiry;
  • canary rotation;
  • metrics untuk active credential version.

15. Failure Mode Matrix

FailureEnvVolumeSidecarRuntime FetchESO/CSI
Secret missing at startupcrash if validatedcrash if file missinginit/agent failapp failpod may fail/missing secret
Secret changesno updatefile may updatefile/template updatedirect refreshsync/mount update
Secret manager down at startupmaybe unaffected if K8s Secret existsmaybe unaffectedaffectedaffectedcontroller/driver affected
Secret manager down at runtimeunaffected but staleunaffected but stalerefresh stopsrefresh failssync/rotation fails
Credential expiresapp unawareapp unaware unless polling metadataagent/app must coordinateapp can be lease-awaredepends on integration
Leak via env dumphigherlowerlowerlower materializationdepends

Untuk production Java microservices, gunakan default berikut kecuali ada alasan kuat.

16.1 Prefer Workload Identity for Cloud Resources

Untuk object storage, queue, KMS, dan cloud API:

Use workload identity / IAM role instead of static access key.

16.2 Prefer Mounted File for Certificates

Untuk TLS cert/key/truststore:

Use mounted file, CSI, or agent-rendered files.
Do not squeeze cert material into env vars unless unavoidable.

16.3 Prefer Runtime/Agent for Dynamic Secrets

Untuk Vault dynamic DB credential:

Use Spring Cloud Vault lease-aware integration or Vault Agent + app reload strategy.
Do not treat short-lived dynamic secret like static env var.

16.4 Prefer External Secret Operator for GitOps Static Secrets

Untuk static API token yang authority-nya external secret manager:

Use ExternalSecret spec in Git.
Materialize Kubernetes Secret in cluster.
Consume via env/volume with rollout strategy.

16.5 Never Put Plaintext Secret in Git

Bahkan private repo bukan secret manager.

Jika GitOps butuh secret:

  • SOPS;
  • Sealed Secrets;
  • External Secrets Operator;
  • cloud KMS encryption;
  • Vault-backed delivery.

17. Java Redaction Boundary

Apa pun injection pattern-nya, Java code harus redaction-aware.

public record DatabaseCredential(
    String username,
    RedactedValue password,
    Optional<Instant> expiresAt,
    Optional<String> version
) {}

public final class RedactedValue {
    private final String value;

    private RedactedValue(String value) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Secret value is required");
        }
        this.value = value;
    }

    public static RedactedValue of(String value) {
        return new RedactedValue(value);
    }

    public String reveal() {
        return value;
    }

    @Override
    public String toString() {
        return "[REDACTED]";
    }
}

Structured logging filter:

public final class SecretRedactor {
    private static final Pattern SENSITIVE_KEYS = Pattern.compile(
        "(?i)(password|secret|token|api[_-]?key|authorization|credential)"
    );

    public static Object redact(String key, Object value) {
        if (key != null && SENSITIVE_KEYS.matcher(key).find()) {
            return "[REDACTED]";
        }
        return value;
    }
}

18. Observability Requirements

Secret injection harus punya metrics.

Contoh:

secret_load_success_total{secret="db"}
secret_load_failure_total{secret="db", reason="missing_file"}
secret_last_loaded_timestamp_seconds{secret="db"}
secret_seconds_until_expiry{secret="db"}
secret_reload_success_total{secret="db"}
secret_reload_failure_total{secret="db"}
secret_stale_seconds{secret="db"}
secret_version_info{secret="db", version="v12"}

Jangan expose value. Expose metadata aman:

  • loaded yes/no;
  • expiry timestamp;
  • version hash prefix non-reversible jika perlu;
  • lease renewable yes/no;
  • refresh result;
  • error class.

Health check harus hati-hati.

Buruk:

/health returns DB_PASSWORD=...

Baik:

{
  "status": "UP",
  "components": {
    "secrets": {
      "status": "UP",
      "details": {
        "dbCredentialLoaded": true,
        "dbCredentialExpiresInSeconds": 832,
        "lastRefreshStatus": "SUCCESS"
      }
    }
  }
}

19. Design Review Checklist

Gunakan checklist ini sebelum memilih injection pattern.

Authority

  • Siapa source of truth secret?
  • Apakah secret static atau dynamic?
  • Apakah secret punya TTL/lease?
  • Apakah secret bisa direvoke?
  • Apakah akses secret diaudit?

Delivery

  • Apakah melalui env, file, sidecar, CSI, API, atau controller sync?
  • Apakah secret disimpan sebagai Kubernetes Secret?
  • Apakah plaintext pernah masuk Git?
  • Apakah update semantics dipahami?
  • Apakah subPath dihindari untuk secret yang perlu update?

Consumption

  • Apakah Java app hanya baca saat startup?
  • Apakah client library mendukung refresh?
  • Apakah connection pool bisa rotate?
  • Apakah reload atomic?
  • Apakah stale secret punya batas waktu?

Security

  • Apakah secret bisa bocor via log/metrics/traces?
  • Apakah actuator dibatasi?
  • Apakah RBAC least privilege?
  • Apakah file permission benar?
  • Apakah heap dump policy aman?

Operations

  • Bagaimana rotation dilakukan?
  • Bagaimana rollback?
  • Apa alert jika refresh gagal?
  • Apa runbook jika secret expired?
  • Apa yang terjadi jika secret manager unavailable?

20. Key Takeaways

Secret injection adalah keputusan boundary, bukan sekadar YAML.

Prinsip utamanya:

  1. Delivery plane is not authority.
  2. Environment variables are simple but startup-bound.
  3. Mounted files reduce env exposure but require reload coordination.
  4. Sidecars/agents can handle secret retrieval, but Java still owns consumption lifecycle.
  5. Runtime API fetch is powerful for dynamic secrets but couples app to secret authority.
  6. External operators are excellent for GitOps, but create materialized Kubernetes Secret copies.
  7. Workload identity is often better than injecting static cloud credentials.
  8. Secret rotation is a chain: authority, delivery, app observation, client refresh, old credential revocation.
  9. No pattern is safe if secret leaks through logs, traces, actuator, or exception messages.

Part berikutnya akan masuk lebih dalam ke HashiCorp Vault for Java Services: auth method, token, lease, dynamic secret, Spring Cloud Vault, connection pool refresh, dan failure modelling.


References

Lesson Recap

You just completed lesson 47 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.