Updating a Secret in Kubernetes with the Java Client

Featured image for sharing metadata for article

While migrating a number of Java services to Kubernetes, I needed to replace handling of secrets on the filesystem with moving them into a secret management solution.

Instead of needing to learn how to operate and maintain a secret management tool like Hashicorp Vault securely, I decided to use Kubernetes' built in secret management.

I found that it wasn't super straightforward how to use it with the Java SDK, nor to set up a service account for the service to write the secret.

The below has been tested with Kubernetes Java SDK v13.0.1 and Kubernetes server API v1.21.

Service Account

Note This still allows access to the whole namespace, so is not very secure - I've not looked into it very much, but if you have improvements please let me know!

To make sure that only this specific application can write to the secrets, we create a new service account:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: post-deploy-sa
  namespace: www-api
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: post-deploy-role-secret-rw
  namespace: www-api
rules:
- apiGroups:
  - ''
  resources:
  - secrets
  verbs:
  - 'get'
  - 'update'
  - 'patch'
  - 'create'
  - 'list'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: post-deploy-secret-rw-binding
  namespace: www-api
subjects:
- kind: ServiceAccount
  name: post-deploy-sa
  namespace: www-api
roleRef:
  kind: Role
  name: post-deploy-role-secret-rw
  apiGroup: rbac.authorization.k8s.io

And then in the deployment configuration for the application itself, we need to make sure we bind it to the deployment through serviceAccountName:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: post-deploy
  name: post-deploy
  namespace: www-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: post-deploy
  strategy: {}
  template:
    metadata:
      labels:
        app: post-deploy
    spec:
      serviceAccountName: post-deploy-sa
      containers:
  # ...

SDK Code

The Kubernetes SDK has the handy patchNamespacedSecret method, which requires a V1Patch, but it's not super clear that the argument to the constructor needs to be a raw JSON Patch:

// assuming we have an API client prepared
CoreV1Api api = new CoreV1Api();

// encode the secret into Base64
String rawValue = "this is a super secret value";
String encoded = Base64.getEncoder().encodeToString(rawValue.getBytes(StandardCharsets.UTF_8));

V1Patch patch = new V1Patch("[{\"op\": \"replace\", \"path\": \"/data/foo\", \"value\": \"" + encoded "\"}]");
V1Secret secret =
  api.patchNamespacedSecret(
      "post-deploy-secrets", "www-api", patch, null, null, "ThisClassName.class", null);
// `secret` has the updated data

You'll notice that this isn't super readable, nor will it be super fun to maintain as it's escaped JSON.

Also notice that we've got the whole JSON Patch element wrapped in an array, as there may be many operations.

The solution I ended up with was a helper class that can handle the encoding to Base64, and then serialised more easily:

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class PatchBody {
  private static final String REPLACE_OPERATION = "replace";
  private final String encodedSecret;
  private final String path;

  public PatchBody(String rawValue, String path) {
    this.encodedSecret =
        Base64.getEncoder().encodeToString(rawValue.getBytes(StandardCharsets.UTF_8));
    this.path = "/data/" + path;
  }

  public String getOp() {
    return REPLACE_OPERATION;
  }

  public String getPath() {
    return path;
  }

  public String getValue() {
    return encodedSecret;
  }
}

This then allows us to do something like:

// assuming we have an API client prepared
CoreV1Api api = new CoreV1Api();
ObjectMapper mapper = new ObjectMapper();

// encode the secret into Base64
PatchBody patch = new PatchBody("this is a super secret value", "foo");
V1Patch patch = new V1Patch(mapper.writeValueAsString(List.of(patch)));
V1Secret secret =
  api.patchNamespacedSecret(
      "post-deploy-secrets", "www-api", patch, null, null, "ThisClassName.class", null);
// `secret` has the updated data

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #kubernetes #java.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.