cloud-sre

Mount Google Application Credentials into a Kubernetes Pod without Committing the Key

A reusable EKS and Kubernetes pattern for storing a Google service account JSON in AWS Secrets Manager, mounting it with Secrets Store CSI Driver, and letting Java read it through GOOGLE_APPLICATION_CREDENTIALS.

Jun 26, 2026
KubernetesEKSAWS Secrets ManagerSecrets Store CSIFirebaseGitOps

The pattern is simple: keep the Google service account JSON out of Git, store it in a secret manager, mount it into the Pod as a read-only file, and point GOOGLE_APPLICATION_CREDENTIALS at that file.

This is the setup I use when a backend service needs Firebase Admin SDK, Google Cloud client libraries, or any Java code that calls GoogleCredentials.getApplicationDefault().

The examples below are anonymized. Replace every placeholder before applying them.

Terms Used Here

TermMeaning
GOOGLE_APPLICATION_CREDENTIALSEnvironment variable used by Google client libraries to find a service account JSON file.
Secrets Store CSI DriverKubernetes CSI driver that mounts secrets from external stores, such as AWS Secrets Manager, into a Pod as files.
SecretProviderClassKubernetes custom resource that tells the CSI driver which external secret to fetch and which filename to expose.
GitOpsA deployment flow where Kubernetes manifests are committed to Git and synced by a controller such as ArgoCD.

Target Shape

At runtime, the container should have:

GOOGLE_APPLICATION_CREDENTIALS=/mnt/secrets-store/google-application-credentials.json

And the file should exist in the Pod:

/mnt/secrets-store/google-application-credentials.json

The service account JSON itself should live only in the external secret store, not in the GitOps repository.

1. Store the JSON in AWS Secrets Manager

Create one secret per environment and purpose. Use a name that makes the owner and use case clear.

aws secretsmanager create-secret \
  --region <region> \
  --profile <profile> \
  --name <env>/shared/google_application_credentials_<app> \
  --description "Google service account credentials for <app>" \
  --secret-string file://./google-service-account.json

Do not paste the JSON into a Kubernetes manifest. Do not commit the JSON file to the repository.

If the secret already exists, rotate the value with:

aws secretsmanager put-secret-value \
  --region <region> \
  --profile <profile> \
  --secret-id <env>/shared/google_application_credentials_<app> \
  --secret-string file://./google-service-account.json

2. Allow the Workload Role to Read the Secret

The IAM role used by the Pod or by the CSI driver must be able to read the secret.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
        "secretsmanager:ListSecretVersionIds"
      ],
      "Resource": "arn:aws:secretsmanager:<region>:<account-id>:secret:<env>/shared/google_application_credentials_<app>*"
    }
  ]
}

Keep the resource scope narrow. A shared wildcard such as <env>/shared/google_application_credentials* is workable when the same role already owns that small group of Google credential secrets.

3. Add a SecretProviderClass

Create a SecretProviderClass in the same namespace as the workload.

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: <app>-google-application-credentials
  namespace: <namespace>
spec:
  provider: aws
  parameters:
    objects: |
      - objectName: "<env>/shared/google_application_credentials_<app>"
        objectType: "secretsmanager"
        objectAlias: "google-application-credentials.json"

objectName is the AWS Secrets Manager name. objectAlias is the filename that appears inside the Pod.

4. Mount the Secret into the Deployment

Add the environment variable, the read-only volume mount, and the CSI volume.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <app>
  namespace: <namespace>
spec:
  template:
    spec:
      serviceAccountName: <workload-service-account>
      containers:
        - name: <app>
          image: <image>
          env:
            - name: GOOGLE_APPLICATION_CREDENTIALS
              value: /mnt/secrets-store/google-application-credentials.json
          volumeMounts:
            - name: secrets-store
              mountPath: /mnt/secrets-store
              readOnly: true
      volumes:
        - name: secrets-store
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: <app>-google-application-credentials

If the workload already has config and application-secret volumes, keep this as a separate secrets-store volume. The Google JSON is a credential file, not an application YAML override.

5. Include It in Kustomize

For a Kustomize-based GitOps layout:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: <namespace>

resources:
  - secret-provider-class.yaml
  - deployment.yaml
  - external-secret.yaml
  - pdb.yaml

Commit only the manifests. The JSON key stays in AWS Secrets Manager.

6. Java Usage

For Firebase Admin SDK or Google Cloud libraries, prefer Application Default Credentials:

FirebaseOptions options = FirebaseOptions.builder()
    .setCredentials(GoogleCredentials.getApplicationDefault())
    .build();

FirebaseApp app = FirebaseApp.initializeApp(options);

With GOOGLE_APPLICATION_CREDENTIALS set, the Java process reads the mounted JSON file automatically.

If Firebase should be optional in some environments, gate the configuration with a property:

@Configuration
@ConditionalOnProperty(name = "firebase.enabled", havingValue = "true")
public class FirebaseConfig {
    // Firebase beans
}

Then enable it in the environment config:

firebase:
  enabled: true

7. Verify the GitOps Render

Before pushing:

kubectl kustomize <path-to-kustomize-dir>

Check that the rendered manifest contains:

GOOGLE_APPLICATION_CREDENTIALS
secretProviderClass: <app>-google-application-credentials

Then push the GitOps change and let ArgoCD or the equivalent controller sync it.

8. Verify the Cluster

After sync:

kubectl -n <namespace> rollout status deployment/<app> --timeout=120s

Confirm the SecretProviderClass:

kubectl -n <namespace> get secretproviderclass <app>-google-application-credentials \
  -o jsonpath='{.spec.parameters.objects}{"\n"}'

Confirm the environment variable:

kubectl -n <namespace> get deployment <app> \
  -o jsonpath='{range .spec.template.spec.containers[0].env[*]}{.name}{"="}{.value}{"\n"}{end}'

Confirm the file exists without printing the secret:

kubectl -n <namespace> exec deployment/<app> -- sh -c \
  'test -s "$GOOGLE_APPLICATION_CREDENTIALS" && echo credential_file_present'

The last command checks that the file exists and is not empty. It does not output the JSON.

Common Failures

SymptomCheck
Pod fails to mount the volumeCheck the Secrets Store CSI Driver and AWS provider are installed.
Mount fails with AccessDeniedCheck the IAM role can read the exact Secrets Manager ARN.
File exists but Java still failsCheck GOOGLE_APPLICATION_CREDENTIALS points to the exact mounted filename.
Firebase bean is missingCheck firebase.enabled=true or the equivalent application flag.
New secret value is not picked upRestart or redeploy the workload unless secret rotation is explicitly enabled and verified in the cluster.

The useful boundary is this: infrastructure should supply a file path, and application code should use the default Google credential chain. That keeps secret handling out of the codebase and keeps the deployment behavior testable from Kubernetes.