Skip to content

SecretLeaseCreatedEvent throws NullPointerException for secrets containing null values (regression in 4.0.0) #1018

Description

@BeBitbox

Affected versions

Regression introduced in 4.0.0. Confirmed still present in 4.1.0.
Working in 3.2.0.

Description

SecretLeaseCreatedEvent copies the secret data with Map.copyOf(secrets),
which rejects null values with a NullPointerException:

https://github.com/spring-projects/spring-vault/blob/main/spring-vault-core/src/main/java/org/springframework/vault/core/lease/event/SecretLeaseCreatedEvent.java

public SecretLeaseCreatedEvent(RequestedSecret requestedSecret, Lease lease, Map<String, Object> secrets) {
    super(requestedSecret, lease);
    this.secrets = Map.copyOf(secrets);   // NPE if any value is null
}

3.2.0 used a null-tolerant copy:

this.secrets = Collections.unmodifiableMap(new LinkedHashMap<>(secrets));

SecretLeaseRotatedEvent extends SecretLeaseCreatedEvent and delegates to
this constructor, so the rotation path fails identically.

Reproducer

No Vault instance required — the constructor alone reproduces it:

Map<String, Object> secrets = new LinkedHashMap<>();
secrets.put("access_key", "SOME_KEY");
secrets.put("secret_key", "SOME_VALUE);
secrets.put("session_token", null);

// throws NullPointerException on 4.0.0+, succeeds on 3.2.0
new SecretLeaseCreatedEvent(RequestedSecret.renewable("aws/creds/my-role"), Lease.none(), secrets);

Real-world trigger

Vault's AWS secrets engine returns a null session token for the iam_user
credential type, because no STS call is involved:

{
  "data": {
    "access_key": "AKIA...",
    "secret_key": "...",
    "session_token": null
  }
}

How it surfaces

Via Spring Cloud Vault (spring-cloud-vault-config 5.0.2), with
spring.cloud.vault.aws.enabled=true:

SecretLeaseContainer.doGetSecrets      // reads aws/creds/<role>
SecretLeaseContainer.start             // -> onSecretsObtained(...)
SecretLeaseEventPublisher.onSecretsObtained  // -> dispatch(new SecretLeaseCreatedEvent(...))
SecretLeaseCreatedEvent.<init>         // NullPointerException

doGetSecrets has a try/catch (RuntimeException) that routes failures to
onError, but it wraps only the HTTP read — onSecretsObtained is invoked
afterwards, outside it. The NPE therefore escapes unwrapped rather than
reaching any LeaseErrorListener, so the application fails to start with a
bare NullPointerException and no indication of which secret or key caused it.

Expected behaviour

Either:

  1. Restore the null-tolerant copy from 3.2.0, or
  2. Drop null-valued entries before copying, or
  3. At minimum, route the failure through onError so it surfaces as a
    VaultException naming the offending path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions