% Securing Spring Boot with OpenShift and OAuth Part 2 % Andrew DeMaria % 2018-06-04
Quickly securing an application has become quite simple with Spring Boot. However, if you're running on OpenShift - it can be even easier by relying on the builtin OAuth resource server that OpenShift provides. In this multipart blog, we'll take a look at how this can be done.
- Deploying an OAuth Spring Boot Example Application onto OpenShift
- The Basic configuration for OAuth, Spring Boot and OpenShift
- AUTOMATIC!! configuration for OAuth, Spring Boot and OpenShift
- Authorities Extractor for OpenShift OAuth
- Interacting with the OpenShift API using an OAuth Token
Remember that in Part 2, we had to manually configure a couple of properties in our application.yml:
security:
oauth2:
client:
clientId: REPLACE_ME
clientSecret: REPLACE_ME
accessTokenUri: https://openshift.default.svc/oauth/token
userAuthorizationUri: REPLACE_ME
tokenName: access_token
authenticationScheme: header
clientAuthenticationScheme: query
scope: "user:info user:check-access user:list-projects"
resource:
userInfoUri: https://openshift.default.svc/oapi/v1/users/~
Instead of configuring the REPLACE_ME items manually, we can inject these properties into our application at runtime.
In order to do this we will rely on functionality within the
OpenShift OpenJDK S2I image: redhat-openjdk-18/openjdk18-openshift. Namely, we will use the fact that we can supply a
run.sh script that the image will run instead of its normal script, run-java.sh. This functionality can be seen by
taking a look at the image contents via oc run:
oc run -it --restart=Never javatest --image=redhat-openjdk-18/openjdk18-openshift --command -- /bin/bash
If you don't see a command prompt, try pressing enter.
[jboss@javatest ~]$
Since this is an S2I image, we can examine the run script:
[jboss@javatest ~]$ cd /usr/local/s2i/
[jboss@javatest s2i]$ cat run
#!/bin/bash
# Command line arguments given to this script
args="$*"
# Global S2I variable setup
source `dirname "$0"`/s2i-setup
# Always include jolokia-opts, which can be empty if switched off via env
export JAVA_OPTIONS="${JAVA_OPTIONS:+${JAVA_OPTIONS} }$(/opt/jolokia/jolokia-opts) $(source /opt/hawkular/hawkular-opts && get_hawkular_opts)"
if [ -f "${DEPLOYMENTS_DIR}/bin/run.sh" ]; then
echo "Starting the application using the bundled ${DEPLOYMENTS_DIR}/bin/run.sh ..."
exec ${DEPLOYMENTS_DIR}/bin/run.sh $args ${JAVA_ARGS}
else
echo "Starting the Java application using /opt/run-java/run-java.sh ..."
exec /opt/run-java/run-java.sh $args ${JAVA_ARGS}
fi
Looking at the final if statement, we can see that if we send a run.sh script within a bin directory, it will be
run. How can we do this? First, we need to ensure that this script is copied over during the assemble process. I'd
encourage taking a detour and reading the assemble script within the S2I image to see that we can specify
the environment variable, ARTIFACT_COPY_ARGS, to customize what gets copied out of the build process. We will specify
this within our BuildConfig as follows:
- apiVersion: v1
kind: BuildConfig
...
spec:
...
strategy:
sourceStrategy:
env:
- name: ARTIFACT_COPY_ARGS
value: "-r *.jar bin"
forcePull: true
from:
kind: ImageStreamTag
name: redhat-openjdk18-openshift:1.2
namespace: ${IMAGE_STREAM_NAMESPACE}
type: Source
...
So we've added the -r option to recursively copy directories and also added our bin directory. However, note that
the copy command is run relative to the ephemeral target directory that maven creates. So, we will need to copy our
bin directory to the target directory as part of the maven build process. We will utilize two plugins to do this for
us as seen in the pom.xml:
<build>
...
<plugins>
...
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/bin</outputDirectory>
<resources>
<resource>
<directory>bin</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>process-classes</id>
<phase>process-classes</phase>
<configuration>
<target>
<chmod file="target/bin/run.sh" perm="755"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
The first plugin is the maven-resources-plugin which we setup to copy the bin directory into the build directory.
The second plugin is the maven-antrun-plugin which ensures the run.sh script is executable. With these plugins in
place, we can get to creating our run.sh script.
Since our application will be interacting with the OpenShift OAuth API, it needs to trust the OpenShift CA certificate to communicate properly over SSL. The OpenShift CA certificate gets mounted into our running container but is not trusted by default. To trust this certificate, it can be imported into a keystore for our application:
#!/bin/bash
rm -f cacerts
cp /etc/pki/java/cacerts cacerts
chmod u+w cacerts
keytool -import -trustcacerts -alias ocp -file /run/secrets/kubernetes.io/serviceaccount/ca.crt -keystore cacerts -storepass changeit -noprompt
export JAVA_OPTIONS="-Djavax.net.ssl.trustStore=$(readlink -f cacerts) $JAVA_OPTIONS"
In the above, the default cacert is copied to a new file that the runtime user can modify. Then the
Java keytool command is used to add the CA cert at /run/secrets/kubernetes.io/serviceaccount/ca.crt as a
trusted certificate. With this cacert file ready to go, we use it as the javax.net.ssl.trustStore to override the
default location.
Now that our application can securely interact with the OAuth API, we can move on to setting up the User Authorization
URI. Remember in Part 2 that this was the public facing URI for the userAuthorizationUri property. This value can be
discovered by the application through the https://openshift.default.svc/.well-known/oauth-authorization-server
endpoint which returns something like this:
$ curl -k https://192.168.42.46:8443/.well-known/oauth-authorization-server
{
"issuer": "https://192.168.42.46:8443",
"authorization_endpoint": "https://192.168.42.46:8443/oauth/authorize",
"token_endpoint": "https://192.168.42.46:8443/oauth/token",
"scopes_supported": [
"user:check-access",
"user:full",
"user:info",
"user:list-projects",
"user:list-scoped-projects"
],
"response_types_supported": [
"code",
"token"
],
"grant_types_supported": [
"authorization_code",
"implicit"
],
"code_challenge_methods_supported": [
"plain",
"S256"
]
}
In the above, I'm using an external minishift API endpoint - but the internal openshift.default.svc endpoint returns
the same content. The value we're interested in is the authorization_endpoint value. We can pull this out via a
bit of scripting and use it to override the value in the application.yml file
(remember the ordering of Spring Boot property sources allows us to override via Java system variables):
authUri=$(curl -s -k \
https://openshift.default.svc/.well-known/oauth-authorization-server \
| python -c \
'import json,sys;obj=json.load(sys.stdin);print obj["authorization_endpoint"]')
export JAVA_OPTIONS="-Dsecurity.oauth2.client.userAuthorizationUri=${authUri} $JAVA_OPTIONS"
The other two properties we did manually in Part 2 were the clientId and the clientSecret which came from the
service account. For the clientId, we can add the necessary bits as environment variables to the DeploymentConfig.
For the namespace value, we can utilize the downward API to pull it from metadata.namespace:
containers:
- env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: SERVICE_ACCOUNT_NAME
value: ${APPLICATION_NAME}
With these environment variables, we can construct the clientId property in our run.sh script:
export JAVA_OPTIONS="-Dsecurity.oauth2.client.clientId=system:serviceaccount:${NAMESPACE}:${SERVICE_ACCOUNT_NAME} $JAVA_OPTIONS"
Next, we need the service account secret for the clientSecret property. To access this value within the container, we
can change the service account the pod will run with and then OpenShift will mount the secret at
/run/secrets/kubernetes.io/serviceaccount/token during runtime. Changing the service account is done in the
DeploymentConfig in the following snippet where we utilize the fact that the service account we created in Part 2 has
the same name as the APPLICATION_NAME parameter.
- apiVersion: v1
kind: DeploymentConfig
spec:
...
template:
...
spec:
serviceAccountName: ${APPLICATION_NAME}
...
With the serviceAccountName, we tell OpenShift to run the container with our specific service account, rather than the
default one. Now within our run.sh script, we can setup the clientSecret property:
export JAVA_OPTIONS="-Dsecurity.oauth2.client.clientSecret=$(cat /run/secrets/kubernetes.io/serviceaccount/token) $JAVA_OPTIONS"
Putting all of this together, the run.sh script now looks like:
#!/bin/bash
rm -f cacerts
cp /etc/pki/java/cacerts cacerts
chmod u+w cacerts
keytool -import -trustcacerts -alias ocp -file /run/secrets/kubernetes.io/serviceaccount/ca.crt -keystore cacerts -storepass changeit -noprompt
export JAVA_OPTIONS="-Djavax.net.ssl.trustStore=$(readlink -f cacerts) $JAVA_OPTIONS"
export JAVA_OPTIONS="-Dsecurity.oauth2.client.clientId=system:serviceaccount:${NAMESPACE}:${SERVICE_ACCOUNT_NAME} $JAVA_OPTIONS"
export JAVA_OPTIONS="-Dsecurity.oauth2.client.clientSecret=$(cat /run/secrets/kubernetes.io/serviceaccount/token) $JAVA_OPTIONS"
authUri=$(curl -s -k \
https://openshift.default.svc/.well-known/oauth-authorization-server \
| python -c \
'import json,sys;obj=json.load(sys.stdin);print obj["authorization_endpoint"]')
export JAVA_OPTIONS="-Dsecurity.oauth2.client.userAuthorizationUri=${authUri} $JAVA_OPTIONS"
exec /opt/run-java/run-java.sh $*
Notice that at the end of the script, we call the same script that is executed by default if we had not provided our own
run.sh script. In addition, we are utilizing the functionality of the run-java.sh script to pull in our changes to
the JAVA_OPTIONS environment variable.
With this part, we have improved upon the configuration done in Part 2 to make our OpenShift OAuth configuration portable and automatic. In Part 4, we'll take a look at adding an OpenShift Authorities Extractor.