Add secrets module: Spring Cloud Vault, AWS Secrets Manager, Kubernetes configtree and Jasypt on Spring Boot 4.1.2, with real transcripts
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6tTs7ivgTNq4ZrurU
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.104 s -- in com.ankurm.secrets.JasyptTest
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.806 s -- in com.ankurm.secrets.VaultRotationTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.763 s -- in com.ankurm.secrets.KubernetesConfigtreeTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 13.25 s -- in com.ankurm.secrets.AwsSecretsManagerTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.317 s -- in com.ankurm.secrets.LeaksTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.828 s -- in com.ankurm.secrets.DynamicPropertyTrapTest
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.303 s -- in com.ankurm.secrets.VaultImportTest
|
||||
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
|
||||
@@ -0,0 +1,6 @@
|
||||
Vault server: hashicorp/vault:2.2.1
|
||||
spring.config.import = vault:// (default context: secret/${spring.application.name} = secret/secrets-demo)
|
||||
bound: DbProperties[url=null, username=app, password=<redacted:17 chars>]
|
||||
property sources that hold db.password (first one wins):
|
||||
configurationProperties
|
||||
secret/secrets-demo
|
||||
@@ -0,0 +1,17 @@
|
||||
1. spring.cloud.vault.kv.application-name=no-such-app (nothing stored at secret/no-such-app), fail-fast off:
|
||||
started: yes; db.password = null
|
||||
vault property sources mounted: [secret/no-such-app, secret/application]
|
||||
2. same, spring.cloud.vault.fail-fast=true:
|
||||
started: yes; db.password = null
|
||||
3. same, with the strict profile (RequireDbPassword guard in application code):
|
||||
started: no; IllegalStateException: db.password is not configured: refusing to start
|
||||
4. wrong token, fail-fast off (the default):
|
||||
started: yes; db.password = null
|
||||
wrong token, fail-fast=true:
|
||||
started: no; Forbidden: 403 Forbidden on GET request for "http://localhost:<port>/v1/secret/secrets-demo": "{"errors":["2 errors occurred:\n\t* permission denied\n\t* invalid token\n\n"]}<EOL>"
|
||||
5. Vault unreachable (127.0.0.1:1), import = vault://, fail-fast off:
|
||||
started: yes; db.password = null
|
||||
Vault unreachable, fail-fast=true:
|
||||
started: no; HttpHostConnectException: Connect to http://127.0.0.1:1 failed: Connection refused (connect failed)
|
||||
6. Vault unreachable, import = optional:vault://, fail-fast=true:
|
||||
started: no; HttpHostConnectException: Connect to http://127.0.0.1:1 failed: Connection refused (connect failed)
|
||||
@@ -0,0 +1,10 @@
|
||||
started with db.password = password-v1
|
||||
-> vault kv put secret/secrets-demo db.password=password-v2
|
||||
environment, no refresh: db.password = password-v1
|
||||
DbProperties bean, no refresh: password-v1
|
||||
@RefreshScope bean, no refresh: password-v1
|
||||
ContextRefresher.refresh() changed keys: [db.password]
|
||||
environment, after refresh: db.password = password-v2
|
||||
DbProperties record bean, after refresh: password-v1
|
||||
@RefreshScope bean, after refresh: password-v2
|
||||
old version is still in Vault: vault kv get -version=1 -> password-v1
|
||||
@@ -0,0 +1,6 @@
|
||||
policy app-read (vault/app-read.hcl) attached to a 1h token
|
||||
application with that token: started, db.password = password-v1
|
||||
same token, vault kv get secret/other-team:
|
||||
* permission denied
|
||||
same token, vault kv put secret/secrets-demo db.password=hijacked:
|
||||
* permission denied
|
||||
@@ -0,0 +1,18 @@
|
||||
moto: created secret /secrets/secrets-demo (JSON with db.password, db.username)
|
||||
spring.config.import = aws-secretsmanager:/secrets/secrets-demo
|
||||
bound: DbProperties[url=null, username=app, password=<redacted:15 chars>]
|
||||
property sources that hold db.password: [configurationProperties, aws-secretsmanager:/secrets/secrets-demo]
|
||||
-> PutSecretValue with db.password=aws-password-v2
|
||||
environment, no reload: db.password = aws-password-v1
|
||||
secret version with stages=[AWSCURRENT]
|
||||
secret version with stages=[AWSPREVIOUS]
|
||||
restart with reload.strategy=refresh, reload.period=PT1S: db.password = aws-password-v2
|
||||
-> PutSecretValue with db.password=aws-password-v3
|
||||
environment after 10 s of 1 s polling: db.password = aws-password-v2
|
||||
ContextRefresher.refresh() changed keys: [db.password]
|
||||
environment after manual refresh: db.password = aws-password-v3
|
||||
@RefreshScope bean after manual refresh: aws-password-v3
|
||||
missing secret, import = aws-secretsmanager:/secrets/nope:
|
||||
started: no; ResourceNotFoundException: Secrets Manager can't find the specified secret. (Service: SecretsManager, Status Code: 404, Request ID: <id> (SDK Attempt Count: 1)
|
||||
missing secret, import = optional:aws-secretsmanager:/secrets/nope:
|
||||
started: yes; db.password = null
|
||||
@@ -0,0 +1,22 @@
|
||||
k8s/secret.yaml data[db.password] = czNjcjN0LWZyb20tazhz
|
||||
base64 -d -> s3cr3t-from-k8s (encoding, not encryption: anyone with 'get secrets' can do this)
|
||||
mounted directory, as the kubelet lays it out:
|
||||
..2026_11_08_10_00_00.111111111/
|
||||
..data -> ..2026_11_08_10_00_00.111111111
|
||||
db.password -> ..data/db.password
|
||||
db.username -> ..data/db.username
|
||||
spring.config.import = configtree:<mount>/
|
||||
bound: DbProperties[url=null, username=app, password=<redacted:15 chars>]
|
||||
config tree property source: Config tree '<mount>'
|
||||
keys it exposes: [db.password, db.username]
|
||||
-> kubelet-style update: new generation dir, ..data link swapped atomically
|
||||
environment after the swap: db.password = s3cr3t-from-k8s
|
||||
DbProperties after the swap: s3cr3t-from-k8s
|
||||
@RefreshScope bean after swap: s3cr3t-from-k8s
|
||||
ContextRefresher.refresh() changed keys: [db.password]
|
||||
environment after refresh: db.password = s3cr3t-rotated
|
||||
@RefreshScope bean after refresh: s3cr3t-rotated
|
||||
directory missing, import = configtree:/no/such/dir/:
|
||||
started: no; ConfigDataResourceNotFoundException: Config data resource 'config tree [/no/such/dir]' via location 'configtree:/no/such/dir/' cannot be found
|
||||
directory missing, import = optional:configtree:/no/such/dir/:
|
||||
started: yes; db.password = null
|
||||
@@ -0,0 +1,14 @@
|
||||
StringEncryptor beans: [lazyJasyptStringEncryptor]
|
||||
encrypt("jasypt-secret") twice with the starter's default encryptor:
|
||||
L3j9uk0JtxuSgfME2mgcEdp4DqxtdXsrHnLaQ2dXyIEOEwzHncjtN3MJm0praytC
|
||||
QcEwZQiMm5z1a1VIf17Gkbr9e0eunC0Riuy7tbvYMZ4zVMylXVoBek699CM2LRog
|
||||
same ciphertext both times: false (random salt and IV per call)
|
||||
db.password=ENC(...) + jasypt.encryptor.password=master-key:
|
||||
environment: db.password = jasypt-secret
|
||||
bound: DbProperties[url=null, username=null, password=<redacted:13 chars>]
|
||||
same ENC(...) value, wrong master key:
|
||||
started: no; EncryptionOperationNotPossibleException: null
|
||||
same ENC(...) value, no master key configured:
|
||||
started: no; IllegalStateException: either 'jasypt.encryptor.password', one of ['jasypt.encryptor.private-key-string', 'jasypt.encryptor.private-key-location'] for asymmetric encryption, or one of ['jasypt.encryptor.gcm-secret-key-string', 'jasypt.encryptor.gcm-secret-key-location', 'jasypt.encryptor.gcm-secret-key-password'] for AES/GCM encryption must be provided for Password-based or Asymmetric encryption
|
||||
outside Spring, org.jasypt only, algorithm PBEWITHHMACSHA512ANDAES_256, 1000 iterations, random salt+IV:
|
||||
decrypt(ciphertext) = jasypt-secret
|
||||
@@ -0,0 +1,11 @@
|
||||
1. a record's generated toString:
|
||||
NaiveDb[url=jdbc:postgresql://db/app, password=hunter2]
|
||||
DbProperties overrides toString: DbProperties[url=jdbc:postgresql://db/app, username=app, password=<redacted:7 chars>]
|
||||
2. /actuator/env and /actuator/configprops, Boot 4.1.2 defaults:
|
||||
GET /actuator/env/db.password value shown: ******
|
||||
GET /actuator/env/internal.signing.material value shown: ******
|
||||
GET /actuator/configprops body contains the password: false
|
||||
3. same, management.endpoint.env.show-values=ALWAYS and configprops.show-values=ALWAYS:
|
||||
GET /actuator/env/db.password value shown: hunter2
|
||||
GET /actuator/env/internal.signing.material value shown: also-secret-but-not-named-like-one
|
||||
GET /actuator/configprops body contains the password: true
|
||||
@@ -0,0 +1,5 @@
|
||||
@SpringBootTest properties: spring.config.import=vault://, spring.cloud.vault.token=root
|
||||
@DynamicPropertySource sets spring.cloud.vault.uri to the container's mapped port
|
||||
environment spring.cloud.vault.uri = http://localhost:<port>
|
||||
a secret is stored in that Vault at secret/secrets-demo
|
||||
environment db.password = null
|
||||
@@ -0,0 +1,12 @@
|
||||
1. the jar: src/main/resources/application-leaky.yml (a deliberately committed secret) as packaged
|
||||
$ unzip -p target/secrets-1.0.0.jar BOOT-INF/classes/application-leaky.yml | grep password
|
||||
password: hunter2-committed-to-git
|
||||
2. command-line argument, as any user on the host sees it
|
||||
$ ps -o args= -p <pid> | tr ' ' '\n' | grep db.password
|
||||
--db.password=arg-secret-789
|
||||
3. environment variables, readable by the same user
|
||||
$ tr '\0' '\n' < /proc/<pid>/environ | grep -E 'DB_PASSWORD_FROM_ENV|SIGNING_KEY'
|
||||
DB_PASSWORD_FROM_ENV=env-secret-123
|
||||
SIGNING_KEY=env-signing-key-456
|
||||
4. /proc/<pid>/cmdline, what the kernel keeps of the arguments
|
||||
--db.password=arg-secret-789
|
||||
Reference in New Issue
Block a user