Securely Store Application Secrets in Oracle Cloud Infrastructure Vault

This guide describes how to create a Java application that accesses secrets in Oracle Cloud Infrastructure Vault.

Instead of storing a database URL, username, and password in plain text or environment variables, a secret manager provides a convenient way to store API keys, passwords, certificates, and other sensitive data while improving security.

Oracle Cloud Infrastructure Vault lets you securely store and retrieve secrets such as passwords or other information that shouldn’t be accessible in cleartext. This guide describes how to use Vault to store database connection information for a Micronaut application that uses MySQL, and uses the Micronaut support for Oracle Cloud Infrastructure to make the process seamless.

Prerequisites #

  • JDK 17 or higher. See Setting up Your Desktop.
  • An Oracle Cloud Infrastructure account. See Setting up Your Cloud Accounts.
  • The Oracle Cloud Infrastructure CLI installed with local access configured.
  • An Oracle Cloud Infrastructure compartment with appropriate permission granted to your Oracle Cloud Infrastructure user account to manage the Vault family in the compartment.
  • An Oracle Cloud Infrastructure compartment with appropriate permission granted to your Oracle Cloud Infrastructure user to deploy and manage a database.
  • jq: a lightweight and flexible command-line JSON processor.
  • A Docker-API compatible container runtime such as Rancher Desktop or Docker installed to build a deployable GraalVM Native Executable.
  • The GCN CLI. See Setting up Your Desktop. (Optional.)

Follow the steps below to create the application from scratch. However, you can also download the completed example in Java.

A note regarding your development environment

Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools extension.

Note: If you use IntelliJ IDEA, enable annotation processing.

1. Create the Application #

Create an application using the GCN Launcher.

  1. Open the GCN Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: oci-secrets-demo
    • Base Package: com.example (Default)
    • Clouds: OCI
    • Language: Java (Default)
    • Build Tool: Gradle (Groovy) or Maven
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Database and Secret Management
    • Features: GraalVM Native Image and MySQL
    • Sample Code: Yes (Default)
  3. Click Generate Project. The GCN Launcher creates an application with the default package com.example in a directory named oci-secrets-demo. The application ZIP file will be downloaded in your default downloads directory. Unzip it, open in your code editor, and proceed to the next steps.

Alternatively, use the GCN CLI as follows:

gcn create-app com.example.oci-secrets-demo \
    --clouds=oci \
    --services=database,secretmanagement \
    --features=mysql,graalvm \
    --build=gradle \
    --lang=java
gcn create-app com.example.oci-secrets-demo \
    --clouds=oci \
    --services=database,secretmanagement \
    --features=mysql,graalvm \
    --build=maven \
    --lang=java

For more information, see Using the GCN CLI.

1.1. Configure the Database Application #

Use the Create and Connect a Micronaut Application to an Oracle Cloud Infrastructure MySQL Database guide to create a MySQL database. Follow steps 1.1 (Datasources Configuration) and 2 (Create an Oracle MySQL Database Instance).

2. Create an Oracle Cloud Infrastructure Vault #

Create the Vault using the Oracle Cloud Infrastructure CLI. See the Vault CLI command reference for more information.

There are three required parameters for the create command: compartment OCID, display name, and Vault type.

2.1. Compartment OCID #

Use the same compartment as the one you used for your database, above. You should have saved the compartment OCID as an environment variable, named $C. (See step 2 “Create an Oracle MySQL Database Instance” of Create and Connect a Micronaut Application to an Oracle Cloud Infrastructure MySQL Database.)

2.2. Create the Vault #

Choose a display name (1-100 characters) and a Vault type, either DEFAULT or VIRTUAL_PRIVATE. Choose DEFAULT to avoid the cost of a virtual private Vault.

Run the create command with the compartment OCID, display name, and Vault type substituted:

oci kms management vault create -c $C \
    --display-name gcn_guide_vault \
    --vault-type DEFAULT \
    | jq -r '.data.id'

The jq utility will extract the Vault OCID from the id property in the response; this is the unique identifier for your Vault which you’ll need later. Store the value as the VAULT_ID environment variable:

export VAULT_ID=ocid1.vault.oc1.iad.b5re3....abuwcljryp5dmxbu3si7...

If you use Linux or Mac, you can combine both commands into one, creating the Vault and setting the environment variable at the same time:

export VAULT_ID=$(oci kms management vault create -c $C \
    --display-name gcn_guide_vault \
    --vault-type DEFAULT \
    | jq -r '.data.id')

2.3. Create a Master Encryption Key #

To create a Master Encryption Key, you need the management endpoint URL. It wasn’t available in the response from the Vault create command, so run the get command:

oci kms management vault get --vault-id $VAULT_ID

The response should look like this:

{
  "data": {
    "compartment-id": "ocid1.tenancy.oc1..aaaaaaaaixksuiqo3rx6...",
    "crypto-endpoint": "https://b5re3...-crypto.kms.us-ashburn-1.oraclecloud.com",
    ...
    "display-name": "gcn_guide_vault",
    "id": "ocid1.vault.oc1.iad.b5re3....abuwcljryp5dmxbu3si7...",
    "is-primary": true,
    "lifecycle-state": "ACTIVE",
    "management-endpoint": "https://b5re3...-management.kms.us-ashburn-1.oraclecloud.com",
    ...
  }
}

Wait a few minutes for the Vault to finish provisioning. Re-run the get command until the value of lifecycle-state is ACTIVE. The URL is the value of the management-endpoint property.

Store the URL as the VAULT_ENDPOINT environment variable:

export VAULT_ENDPOINT=https://b5re3...-management.kms.us-ashburn-1.oraclecloud.com

Choose a name for the key, such as “gcn-guide-encryption-key”, and a “Protection Mode”, either SOFTWARE or HSM. Choose SOFTWARE to avoid the cost of using a hardware security module (HSM). Use the AES key shape algorithm and a key length of 32 (256 bits).

Run the create command with the compartment OCID, display name, protection mode, key shape, and management endpoint substituted:

oci kms management key create -c $C \
    --display-name gcn-guide-encryption-key \
    --protection-mode SOFTWARE \
    --key-shape '{"algorithm":"AES","length":"32"}' \
    --endpoint $VAULT_ENDPOINT \
    | jq -r '.data.id'

If you use Windows, escape the inner double quotes in the value for key-shape: '{\"algorithm\":\"AES\",\"length\":\"32\"}

The jq utility will extract the id value from the response:

ocid1.key.oc1.iad.b5re3.....abuwcljrcuie7t6gctc6...

Store the key ID as the VAULT_KEY_ID environment variable:

export VAULT_KEY_ID=ocid1.key.oc1.iad.b5re3.....abuwcljrcuie7t6gctc6...

3. Use Vault with Micronaut #

The micronaut-oracle-cloud subproject provides integration between Micronaut applications and Oracle Cloud Infrastructure, including using Vault as a distributed configuration source. You do not have to add any dependencies manually because you selected the Database and Secret Management services when you created the application with GCN Launcher. The build file includes the following:

oci/build.gradle

implementation("io.micronaut.oraclecloud:micronaut-oraclecloud-vault")

oci/pom.xml

<dependency>
    <groupId>io.micronaut.oraclecloud</groupId>
    <artifactId>micronaut-oraclecloud-vault</artifactId>
    <scope>compile</scope>
</dependency>

4. Configuration Changes #

Modify the file named oci/src/main/resources/bootstrap-oraclecloud.properties so that its contents matches the following:

micronaut.application.name=oci
micronaut.config-client.enabled=true
# <1>
oci.config.instance-principal.enabled=true
oci.vault.config.enabled=true
# <2>
oci.vault.vaults[0].compartment-ocid=${C}
# <3>
oci.vault.vaults[0].ocid=${VAULT_ID}

1 Use Instance Principal authentication to enable the Micronaut application to access the Vault.

2 Set the value of the compartment-ocid property with the OCID unique identifier of the compartment where you created the Vault and secrets.

3 Set the value of the ocid property with the Vault OCID unique identifier you saved when creating the Vault.

Modify the file named oci/src/main/resources/application-oraclecloud.properties so that its contents matches the following:

datasources.default.db-type=mysql
datasources.default.dialect=MYSQL
datasources.default.driver-class-name=com.mysql.cj.jdbc.Driver
flyway.datasources.default.enabled=true
oci.config.profile=DEFAULT

5. Deploy the Application #

Use the Create and Connect a Micronaut Application to an Oracle Cloud Infrastructure MySQL Database guide to create a compute instance and deploy the application to it; follow the steps in the “Create a Compute Instance” and “Deploy from a Native Executable” sections up to the step where you start the application. You need to connect the application to the MySQL database before starting it up.

When creating the Oracle Cloud Infrastructure Compute instance, use the same subnet as the one where you created the Oracle Cloud Infrastructure MySQL database, otherwise the application will not be able to access the database.

6. Configure MySQL Access #

Use the Create and Connect a Micronaut Application to an Oracle Cloud Infrastructure MySQL Database guide to configure access to the MySQL database; follow the steps in the “Configure a MySQL Database” section. You will need the private IP address of the VM, the MySQL private IP address, and the admin username and password you chose when creating the database.

7. Create Secrets #

Database connectivity settings are specified in the application configuration, from values in configuration files, and, as in this guide, from values in a distributed property source such as Vault.

The initial configuration created by the launcher in oci/src/main/resources/application-oraclecloud.properties includes these values:

datasources.default.db-type=mysql
datasources.default.dialect=MYSQL
datasources.default.driver-class-name=com.mysql.cj.jdbc.Driver

In this guide you store values for the username, password, and URL in your Vault by creating Vault secrets for datasources.default.username, datasources.default.password, and datasources.default.url.

7.1. datasources.default.username #

The first secret is for the database username, so the secret name is datasources.default.username.

Secret values must be Base64-encoded. You can encode the value programmatically, for example Base64.getEncoder().encodeToString("the value".getBytes()), or use an online tool such as https://www.base64encode.org/.

Run the create-base64 command with the compartment OCID, encryption key OCID, Vault OCID, secret name, and Base64-encoded secret value substituted. If you use guide_user as the username, the Base64-encoded value will be Z3VpZGVfdXNlcg==.

oci vault secret create-base64 -c $C \
    --key-id $VAULT_KEY_ID \
    --vault-id $VAULT_ID \
    --secret-name datasources.default.username \
    --secret-content-content Z3VpZGVfdXNlcg==

Note: the command leaves the Base64-encoded value in your shell history. To avoid this, create a JSON file containing the parameters and pass that as an argument to the command.

To use this approach, create a file named key.json with values substituted:

{
   "compartmentId": "ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...",
   "keyId": "ocid1.key.oc1.iad.b5re3.....abuwcljrcuie7t6gctc6...",
   "vaultId": "ocid1.vault.oc1.iad.b5re3....abuwcljryp5dmxbu3si7...",
   "secretName": "datasources.default.username",
   "secretContentContent": "Z3VpZGVfdXNlcg=="
}

and run this create-base64 command instead:

oci vault secret create-base64 --from-json file://key.json

With either approach the response should look like this:

{
  "data": {
    "compartment-id": "ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...",
    ...
    "id": "ocid1.vaultsecret.oc1.iad.amaaaaaafzr7royabqgz...",
    "key-id": "ocid1.key.oc1.iad.b5re3.....abuwcljrcuie7t6gctc6...",
    "lifecycle-state": "CREATING",
    "secret-name": "datasources.default.username",
    ...
    "vault-id": "ocid1.vault.oc1.iad.b5re3....abuwcljryp5dmxbu3si7..."
  }
}

7.2. datasources.default.password #

Create a second secret with the name datasources.default.password. The value is the Base64-encoded database user password you chose earlier.

7.3. datasources.default.url #

Create a third secret with the name datasources.default.url. The URL is jdbc:mysql://<MySQL IP address>:3306/<MySQL Database Name> with the private IP address of your MySQL database and name of the database you created (e.g., “gcnDB”) substituted. Set the value of the secret as the Base64-encoded URL value.

8. Instance Principal authentication #

Instance Principal authentication enables the Micronaut application to retrieve secrets from Vault. To use this, create a dynamic group and add a policy statement granting permission.

8.1. Dynamic Group #

Choose a group name, such as “gcn-guide-dg”, and a matching rule (the logic that will be used to determine group membership). For this guide, use a rule that includes all instances in your compartment: instance.compartment.id = 'ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm…' (replacing ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm… with your compartment OCID).

Run the create command with the compartment OCID substituted:

oci iam dynamic-group create \
   --name gcn-guide-dg \
   --description gcn-guide-dg \
   --matching-rule "instance.compartment.id = 'ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm...'"

For more information, see Dynamic Groups.

8.2. Dynamic Group Policy Statements #

Create the policy that grants read access to Vault secrets in your compartment.

Run the create command with your compartment OCID substituted:

oci iam policy create -c $C \
    --name gcn-guide-policy \
    --description gcn-guide-policy \
    --statements '["allow dynamic-group gcn-guide-dg to read secret-family in compartment id ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbm..."]' \
    | jq -r '.data.id'

The jq utility will extract the policy OCID from the id property in the response. Store the value as the POLICY_ID environment variable:

export POLICY_ID=ocid1.policy.oc1..aaaaaaaau7uhwxr3ynlr...

9. Start the application #

  1. Create an executable JAR file including all application’s dependencies:

    ./gradlew :oci:shadowJar
    ./mvnw install -pl lib -am && ./mvnw package -pl oci -DskipTests
  2. From the local terminal, copy the JAR file from your computer to the compute instance using scp:

    scp -i /path/to/ssh-key-*.key path/to/oci-secrets-demo/oci/build/libs/oci-1.0-SNAPSHOT-all.jar opc@[COMPUTE_INSTANCE_PUBLIC_IP]:~/application.jar
           
    scp -i /path/to/ssh-key-*.key path/to/oci-secrets-demo/oci/target/oci-1.0-SNAPSHOT.jar opc@[COMPUTE_INSTANCE_PUBLIC_IP]:~/application.jar
           
  3. Once copied, connect to the compute instance (if it has disconnected by now):
     ssh -i /path/to/ssh-key-*.key opc@[COMPUTE_INSTANCE_PUBLIC_IP]
    
  4. Start the application:
     java -jar application.jar
    
  5. From the local terminal, run this command to test creating and storing a new Genre in the database:
     curl -X "POST" "http://[COMPUTE_INSTANCE_PUBLIC_IP]:8080/genres" \
           -H 'Content-Type: application/json; charset=utf-8' \
           -d $'{ "name": "music" }'
    

    Then list the genres:

     curl [COMPUTE_INSTANCE_PUBLIC_IP]:8080/genres/list
    

    The response should look like this:

     {"id":1,"name":"music"}
    

10. Package with GraalVM Native Image #

GCN supports compiling Java applications ahead-of-time into native executables using GraalVM Native Image and Native Build Tools. Packaged as a native executable, it significantly reduces the application startup time and memory footprint.

Prerequisites: GraalVM Native Image is required to build native executables. Install GraalVM JDK with Native Image if you have not done that yet.

If you build a native executable locally, it will only run on your OS, and isn’t suitable for deployment to a Compute instance. To create a deployable native executable, use a different packaging command that builds the executable inside a container, which you’ll extract from the container to deploy to the cloud.

  1. From the local terminal, generate a native executable by running the following command:

    ./gradlew :oci:dockerBuildNative
    ./mvnw install -pl lib -am && ./mvnw package -pl oci -Dpackaging=docker-native

    You can customize the name of the resulting binary by updating the Maven/Gradle plugin for GraalVM Native Image configuration.

  2. Find the container image ID that you’ll use to extract the native executable from the container image you built by running:

     docker image ls
    

    The images are listed chronologically with the most recent at the top, so your output will look like this:

     REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
     oci          latest   41c8a902b2e7   32 seconds ago   100MB
    
  3. Extract the native executable by running these commands, replacing <image_id> with the ID of the container image ID, for example, 41c8a902b2e7:

     docker create --name container_temp <image_id>
     docker cp container_temp:/app/application .
     docker rm container_temp
    

11. Deploy from a Native Executable #

  1. From the local terminal, copy the native executable from your computer to the Compute instance using scp:

     scp -i /path/to/ssh-key-*.key application opc@[COMPUTE_INSTANCE_PUBLIC_IP]:~/
    
  2. Once copied, connect to the compute instance (if it has disconnected by now):
     ssh -i /path/to/ssh-key-*.key opc@[COMPUTE_INSTANCE_PUBLIC_IP]
    
  3. Stop the application running from the JAR file if it’s still running.

  4. Run the application from the native executable:
     ./application
    
  5. From the local terminal, run this command to test creating and storing a new Genre in the database:
     curl -X "POST" "http://[COMPUTE_INSTANCE_PUBLIC_IP]:8080/genres" \
           -H 'Content-Type: application/json; charset=utf-8' \
           -d $'{ "name": "music" }'
    

    Then list all genres:

     curl [COMPUTE_INSTANCE_PUBLIC_IP]:8080/genres/list
    

    You do not need to install a Java VM to run the application from a native executable. The native executable is a self-contained binary. Deploying from a native executable significantly reduces application startup time and memory footprint.

12. Clean up Cloud Resources #

After you’ve completed this guide, you can clean up the resources you created.

Delete the policy by running the following command:

oci iam policy delete --policy-id $POLICY_ID

To delete the dynamic group, find its OCID by running the list command:

oci iam dynamic-group list | jq -r '.data[] | select(.name=="gcn-guide-dg") | .id'

and run the delete command, substituting the group OCID:

oci iam dynamic-group delete --dynamic-group-id ocid1.dynamicgroup.oc1..aaaaaaaaipoabhhaqnj77urm...

Delete the Vault. You cannot delete it directly; instead request deletion at a date at least seven days in the future. Run this, replacing the date:

oci kms management vault schedule-deletion \
    --vault-id $VAULT_ID \
    --time-of-deletion 2023-04-12

Finally, delete the database it using the CLI. Run the following command:

oci mysql db-system delete \
    --db-system-id ocid1.mysqldbsystem.oc1.iad.aaaaaaaa2pq3a37hftut...

Summary #

This guide demonstrated how to create a Java application that accesses secrets in Oracle Cloud Infrastructure Vault. You also saw how to package this application into a native executable and deploy it from a Compute instance.