Skip to main content

Secret Injection in GitOps Applications

Last updated on

The Harness Argo CD Config Management Plugin resolves secret expressions in your manifests during Argo CD's rendering phase, before anything is applied to the cluster. No Harness Delegate required.

What you will learn from this topic

Expressions and tool detection

This topic covers secrets and security. For Helm and Kustomize detection, non-secret expression types, and the HELM_ARGS warning, go to Harness expressions in GitOps manifests.


Before you begin

Before using secret injection, ensure you have:

  • GitOps Agent: Agent with the plugin enabled. Go to Harness expressions in GitOps manifests for installation steps.
  • Secrets configured: Secrets in Harness Secret Manager, Vault, or another integrated secret manager. Go to Harness Secret Manager to create secrets.
  • Kubernetes manifests: A Git repository containing Kubernetes manifests where you want to inject secrets.

Manifest rendering with the Config Management Plugin

Argo CD normally controls manifest rendering end-to-end. The Config Management Plugin (CMP) intercepts that phase and hands it to Harness, which:

  1. Scans manifests for <+secrets.getValue("ref")> expressions
  2. Resolves secret references against Harness Secret Manager (or Vault/external managers)
  3. Injects actual values into the manifest
  4. Returns the rendered manifest to Argo CD for deployment

Because resolution happens at render time, you do not need to manually sync after adding expressions; Argo CD re-renders on the next sync and the values are there.

Delegate Requirements

The GitOps agent handles secret resolution directly via the CMP sidecar. This makes it viable for pure GitOps setups where you want only the Argo CD stack, without running a Delegate alongside it.


Configure Harness secrets in GitOps

Prerequisites: Plugin enabled on your agent (see setup guide) · Secret exists in Harness.

Step 1. Create a secret in Harness

Go to your project → SecretsNew Secret → Text secret. Note the identifier (e.g., prodDbPassword).

Step 2. Reference it in a kind: Secret manifest

secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: default
type: Opaque
stringData:
db-password: <+secrets.getValue("prodDbPassword")>

Or using a service variable of type Secret:

secret.yaml
stringData:
db-password: <+serviceVariables.dbPassword>
# Resolves: <+serviceVariables.dbPassword> → <+secrets.getValue("account.prodDbPassword")> → actual value

Step 3. Commit, push, sync

The plugin resolves the expression at render time. The deployed Secret in the cluster contains the actual value (base64-encoded as Kubernetes expects).

Verify:

kubectl get secret app-secrets -n default -o jsonpath='{.data.db-password}' | base64 -d

Supported secret sources

SourceConfiguration
Harness Secret ManagerBuilt-in. Create a Text secret, reference by identifier.
HashiCorp VaultConfigure Vault connector in Harness; reference secrets via Harness secret entity pointing to Vault path
Other external managersAny secret manager with a Harness connector

Reference syntax

ScopeSyntax
Project<+secrets.getValue("secretIdentifier")>
Account<+secrets.getValue("account.secretIdentifier")>
Org<+secrets.getValue("org.secretIdentifier")>

Using service/environment variables (two-stage resolution)

When a Harness variable is defined as type Secret, it resolves in two stages:

StageWhat happens
Manifest generation<+serviceVariables.name><+secrets.getValue("account.ref")>
Deployment time<+secrets.getValue(...)> → actual secret value injected

Security model

Secret Visibility and Access

Argo CD caches rendered manifests, including injected secret values, in Redis

The rendered manifests (with real secret values) are stored in Argo CD's Redis instance and also exposed via the repo-server gRPC API. Anyone with access to either can read the resolved values.

ComponentWhat it can see
Argo CD RedisCached rendered manifests with injected values
Argo CD repo-server APISame cached manifests via gRPC
Harness UISecret values are masked
Kubernetes clusterSecrets stored base64-encoded; requires cluster RBAC to read

Hardening checklist

  1. Network policies: block direct access to Argo CD's Redis and repo-server from workloads that do not need it. Verify your cluster enforces these policies.
  2. Dedicated Argo CD cluster: run Argo CD on a cluster with no other tenant workloads; minimizes lateral movement risk.
  3. RBAC: restrict who can read Kubernetes Secret resources and who can access Argo CD components.
  4. Audit logs: monitor access to Argo CD components for unauthorized reads.

For more, see Argo CD secret management: mitigating risks of injection plugins.

Best practices

  • Define secrets in Harness before referencing them in manifests
  • Test in non-production environments before relying on secret injection in production
  • Use RBAC to control who can modify secret expressions in Application manifests
  • Avoid HELM_ARGS: it passes unsanitized strings to the shell. See HELM_ARGS warning

Limitations

Application source type is reported as Plugin

When the CMP is active, Argo CD reports source type as Plugin instead of Helm/Directory. Features that rely on source type detection (Helm chart path listing, auto-type detection) do not work. Go to the upstream issue for details.

Workaround: Specify application source paths manually instead of relying on Argo CD's path discovery UI.

Kustomize secretGenerator is unverified

Secret injection into kind: Secret objects produced by Kustomize's secretGenerator has not been verified. secretGenerator output is already base64-encoded before the plugin sees it, and the plugin does not re-encode substituted values. Use plain Secret manifests (not secretGenerator) until this is confirmed.


Troubleshooting

Secret not found

secret "secretIdentifier" not found
  1. Confirm the secret exists in Harness (correct scope: project, org, or account).
  2. Check the identifier in the expression matches exactly (case-sensitive, no extra spaces).
  3. Confirm you have permission to read the secret at that scope.

Expression not replaced

<+secrets.getValue("ref")> appears literally in the deployed pod:

  1. Verify the plugin is installed: kubectl get pods -n <agent-namespace> should show argocd-harness-plugin as a running container on the repo-server pod.
  2. Check sidecar logs: kubectl logs -n <agent-namespace> <repo-server-pod> -c argocd-harness-plugin
  3. Confirm expression syntax: <+secrets.getValue("identifier")> with double quotes.

Secret resolves to empty

  1. Secret exists but has no value; check in Harness Secrets.
  2. Access denied; verify RBAC permissions for the GitOps agent service account.

Next steps