Cloud Config¶
So Keep-Watching is a service that's run in the cloud, but must also be debuggable on a local setup.
The gives rise to a requirement about the configuration, which needs to be dynamically loaded based on the context.
Next up, it is using public services only for hosting (sources), building and running. That makes it very difficult to keep things a secret so the secrets must be encrypted.
Spring Cloud Config¶
For Keep-Watching, this is: Keep-Config.
For the external dynamic configuration loading, we utilize Spring Cloud Config Server.
This allows you to store configuration files in a git repository (local or remote) which contains configuration per profile.
For this to work, you need two spring applications and a repository:
- The application for which you want to create the configuration (the Client, Keep-Watching)
- A Spring application which acts as a Cloud Config Server (the Server, Keep-Config)
- Git repository with the configuration (Config)
For how to set this up, there's a nice tutorial from SpringSource.
Keep-Config¶
Make sure its a spring boot app, that contains the cloud config server dependency.
pom.xml¶
1 2 3 4 | <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> |
Application.java¶
The application class should then enable the server with the annotation @EnableConfigServer
1 2 3 4 5 6 7 | @SpringBootApplication @EnableConfigServer public class RestServiceApplication { public static void main(String[] args) { SpringApplication.run(RestServiceApplication.class, args); } } |
And last but not least, it needs to configure where the configuration should come from in application.yml
application.yml¶
1 2 3 4 5 6 7 8 | spring: cloud: config: server: git: uri: https://github.com/joostvdg/config encrypt: enabled: false |
Keep-Watching¶
The client needs to configure that it uses a cloud config server, where it is and that requires a dependency on spring-cloud-starter-config
.
pom.xml¶
1 2 3 4 | <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> |
bootstrap.yml¶
As the configuration for the application should be loaded before the applications starts, this configuration should be in the bootstrap.yml
file.
1 2 3 4 5 6 7 8 9 10 11 12 13 | spring: application: name: keep-watching cloud: config: uri: http://localhost:8081 --- spring: profiles: heroku cloud: config: uri: https://keep-config.herokuapp.com |
And depending on the active profile we will use a different server for this.
Config¶
For Keep-Watching, this is: Config.
THe only thing to do here to create configuration files per application for each profile.
- keep-watching-compose.properties: used when profile is compose
- keep-watching-heroku.properties: used when profile in heroku
- keep-watching.properties: default file used
These files are then just standard Java properties files.
Encryption¶
For how to use the encryption with Spring Cloud Config, there's a great tutorial from Baeldung.
Java Encryption Strength¶
Due to strong export restrictions on cryptography in the U.S.A and Java being from there, Java's default encryption strength is severely limited.
For secure encryption of secrets in public places, such as the configuration being in a GitHub repo, we need to use stronger algorithms.
There's an easy solution to this: download and "install" Java's Java Cryptography Extension(JCE).
Config Server on Heroku¶
As we run our Config Server directly on Heroku, our Heroku host needs to have the Unlimited Strength in place.
This is actually surprisingly simple, as can be seen in this tutorial on heroku's devcenter.
Simply create a .jdk-overlay
folder and "install" the JCE just as you would do locally.
Docker¶
As Keep-Watching runs in a Docker container, it will also need the JCE to be able to use the "Unlimited Strength Policy".
So we use a Docker base image which contains exactly that: anapsix/alpine-java:8_jdk_unlimited.
Spring Configuration¶
We need to add a JKS and configure Spring to use this JKS.
This is exactly the same for the server and the client.
This is what is required in the application.yml
.
1 2 3 4 5 6 | encrypt: keyStore: location: classpath:/config-server.jks password: nothepassword alias: config-server-key secret: nothepassword |
As to be expected, we do not use the actual username and password of the JKS in the sources.
This would undermine the safety of the encryption, see below how this is configured.
Secret Encryption / Decryption¶
To encrypt and decrypt the values, we can use the keep-config application.
To encrypt: curl -X POST --data-urlencode d3v3L http://root:s3cr3t@localhost:8081/encrypt
To decrypt: curl -X POST --data-urlencode d3v3L http://root:s3cr3t@localhost:8081/decrypt
Once you have a encrypted value, you want to store it encrypted in the config repository.
In order to tell the config server this value is encrypted, you have to add a special marker; {cipher}
1 2 | message=Hoi hoi hoi user.password={cipher}AgAuqFORZF2ls7XmjQxotluoVXL7M8kEM8OV8Z9/xBPReuVMCbF5Krcd2qNQRq2/l6gTBrqcQXdy/nnv4dHxxGfDU4fxOAL+6YjPqLpZ13N9UYG8sKBw9UjupltLR3S/xHGXBFPp67WC/OeZ7MLbLqa8chY9UWbSySFcK43kNuTKZYsHfeh6ZZt7rAkjzdLoIAC1k4t1YVZxn4Bx9c3gOEIV9ZH1va+AJHg09xRXslCApUklTx6RRTOPt7G+iRizKZe9cwlZwJXu5Niaujtv8Jo6B8HdCq6c5fh0N4Lvvfohb1pOX/drKJm56zRzklcn/Tz8/xAKS4GsPks++zWdhqJU+xVMqBTD7htglmU3j2VZs2YqrBcw5hojEwQPRgH0e6BiU+IxLCqUolaSmCRgWrtx/Yz+Ft6X8zq3Fa+ater3MhptP40LJDDRiA+Gathvp+YHf7SpToGEea4Mxcx547IwzDqigXgMxhhQwyvI6fzR5IZXxL1kY2mUgIyPpg+xCg2bx4lH9ufGtZCr8AYkjnsZc5LH6DGPaYeWmpYu+LuNuRxVP2OdH1UVXhLL+X35MZq9RBtSTK/9JU1WtRVdc7q+g7YbaE1DKnt/5zteX0sfQO7rs20ATMF5JLM3KglHm27Pv4RSQWl4CEUqtL0AhsE6/pxFaxpZ9LsvnNk5GZu/jPkZlduKyFJneJCG4lg4jc5CAMfuExv9Sx2NyKV4wpSP1Qs9VqyvnUA1BFtOL4nS19kRZigsIZDVBxWS6X5yWIk= |
Warning
If you would now request the values from the server, both will come in plain text. You have to disable the automatic decryption by the config server!
Disable automatic decryption in cloud config server¶
1 2 3 4 5 6 7 8 | spring: cloud: config: server: git: uri: https://github.com/joostvdg/config encrypt: enabled: false |
Decrypt in client¶
When decryption is disabled in the server, we will have to decrypt in the client.
It will need the same JKS as was use for the encryption (see above).
For the decryption to work, one more thing is required: a dependency on spring-security-rsa
.
1 2 3 4 | <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-rsa</artifactId> </dependency> |
JKS Secret & Password¶
Make sure we have an additional line for when we've detected we're running (the docker container) in Heroku:
1 2 3 4 5 | if [ "${DATABASE_URL}" ]; then # ... EXTRA_CONFIG="-Dencrypt.keyStore.secret=${KEYSTORE_SECRET} -Dencrypt.keyStore.password=${KEYSTORE_PASS} -Dspring.profiles.active=heroku" fi java ... ${EXTRA_CONFIG} -jar /app.jar |
Make sure we add the secrets are available as environment variables, so the docker run will be able to use them.
We do the same with building in CircleCI, by adding this to the test command (in test.sh).
1 | mvn flyway:migrate generate-resources generate-sources package -e ... -Dencrypt.keyStore.secret=${JKS_SECRET} -Dencrypt.keyStore.password=${JKS_PASS} |