Skip to content

Latest commit

 

History

History

sops

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

Create a secret container from a structured file

Secret template

Create your secret template from scratch. This will define your secret specification used by the secret consumer.

app:
    {{ .Values.env }}:
        database:
            user: app-{{ randAlpha 16 }}
            password: {{ paranoidPassword }}
        server:
            privacy:
                principal: {{ paranoidPassword | b64enc }}
            http:
                session:
                    cookieKeyB64: {{ paranoidPassword | b64enc }}
                token:
                    signingPrivateKeyJWK: |-
                        {{ $key := cryptoPair "ec:p384" }}{{ $key.Private | toJwk }}
                    signingPublicKeyJWK: |-
                        {{ $key.Public | toJwk }}
        vendor:
            mailgun:
                apiKey: {{ .Values.mailgun.apikey | quote }}

When designing a secret tree, please consider the authorizations applicable to the secrets. Don't expose too many secrets just to make it easy to handle and on the other hand don't split secrets to make it too atomic. Try to bundle them in package where the authorization policy will be applied.

Secret Path Description Specification Cardinality / Rotation Period
app/{{ env }}/database/user Defines the application user used by your application to manage service data. Has the app- prefix to identify app service accounts, and a random alphanumeric 16 characters value as discriminant to handle multi instances of the same service. One per instance / 90 days
app/{{ env }}/database/password Defines the password used to authenticate to application database identity. 64 Printable ASCII characters. One per instance / 7 days
app/{{ env }}/server/privacy/principal Defines the seed used by cryptographic function to anonymize the principal. Standard Base64 encoded 64 Printable ASCII characters. One per environment / No rotation
app/{{ env }}/server/http/session/cookieKeyB64 Defines the encryption key used by cookie encryption function. Standard Base64 encoded 64 Printable ASCII characters. One per environment / 30 days
app/{{ env }}/server/token/signingPrivateKeyJWK Defines the JWT Token signing key encoded using JWK. EC P384 Private Key encoded using JWK. One per environment / 24 hours
app/{{ env }}/server/token/signingPublicKeyJWK Defines the JWT Token verification public key encoded using JWK. EC P384 Public Key encoded using JWK. One per environment / 24 hours
app/{{ env }}/server/vendor/mailgun/apiKey Defines the Mailgun API Key used by the service. Imported value during the rendering. One per environment / 180 days

Sample output

$ harp template --in template.yaml --set env=production --set mailgun.apikey=1234567890
app:
    production:
        database:
            user: app-tNtKnOgYXuuJyArI
            password: (Pzd~O9JQ"Epm)QN-eWZAscJHxjyfQlXXq_t=HwEc1it3Bu9h+A7um777q"t4n*8
        server:
            privacy:
                principal: R0FLZVROZDZlMGRUPFYsQXJRWFluVGlJYUFCTDBIT3p5a145TTRAZjNsc0B6XnhmKGo0djU4Y2AzY01vaDolcA==
            http:
                session:
                    cookieKeyB64: aV5kU292VTYvTlhJM1BrdDgzSEJUcXgrOSVIfkU2Tlp2azN4YXBhWlEpLmJjZiNkZz96VyVvQ0piN3YwbHRpNQ==
                token:
                    signingPrivateKeyJWK: |-
                        {"kty":"EC","kid":"sAt9gTz5oUmTefVK0Ib1OfRv7uKVC1at9bB1VjR52QE=","crv":"P-384","x":"XeCZRaJDh4i1ywansUMAh2kN6WbEqWNnQc0diC0SkVxmCAcxA69PQKbyYJy49z9Y","y":"FuDWJpFuuYX0JDRSJTQ5uexLCU3-G4tGEGAoRtWQLRCZIpz4tfcd87bFiIhAb4MT","d":"gHmrbNC5X8rp4ORxwQkdeqMctMjP_jLO3ngd_oxp-mclK3O7TIpPM0dm7TEx0AjR"}
                    signingPublicKeyJWK: |-
                        {"kty":"EC","kid":"sAt9gTz5oUmTefVK0Ib1OfRv7uKVC1at9bB1VjR52QE=","crv":"P-384","x":"XeCZRaJDh4i1ywansUMAh2kN6WbEqWNnQc0diC0SkVxmCAcxA69PQKbyYJy49z9Y","y":"FuDWJpFuuYX0JDRSJTQ5uexLCU3-G4tGEGAoRtWQLRCZIpz4tfcd87bFiIhAb4MT"}
        vendor:
            mailgun:
                apiKey: "1234567890"

Deploy secrets

HashiCorp Vault

$ harp from object --in production.yaml \
  | harp bundle dump --data-only \
  | jq
{
  "app/production/database": {
    "password": "c7#kaTKgAqjAuC6KKdAz)_tsa9mBetD83lAk2cSvyH.jB0AeW%/m4g~4Vi]|I7x!",
    "user": "app-xhyXgOEzYaPmFtYS"
  },
  "app/production/server/http/session": {
    "cookieKeyB64": "fHJ1N1Z6YWI5RSIuWm1VNHZCTVB4NGdjSCVmJSFseWNwcjVRT2gwWC95TSlmNVVPfTc5X2tmaHp4TVUxSHR0dQ=="
  },
  "app/production/server/http/token": {
    "signingPrivateKeyJWK": "{\"kty\":\"EC\",\"kid\":\"m5xr81U_PIKPLd0uzi6QH_kefkMoiN3fF5OUR34cF5I=\",\"crv\":\"P-384\",\"x\":\"nqrvtZkR8WxH_1URu3rDkd-FtBysDSjfmyUwctEV5xpWH_O71rOCsZDXltB5wBtq\",\"y\":\"0tqE2Atw9OncJsi0gGNw5k2Ay5u3xfCJlVZKPSDjKgqWIOpVYDVFqFN6IFu7qIgX\",\"d\":\"Ln4mPosjcCXRXq7TcKEVoWS8b2kfpCXnT4ifUSFFtAzesLWKo9AUQg1lStt8Oz8P\"}\n",
    "signingPublicKeyJWK": "{\"kty\":\"EC\",\"kid\":\"m5xr81U_PIKPLd0uzi6QH_kefkMoiN3fF5OUR34cF5I=\",\"crv\":\"P-384\",\"x\":\"nqrvtZkR8WxH_1URu3rDkd-FtBysDSjfmyUwctEV5xpWH_O71rOCsZDXltB5wBtq\",\"y\":\"0tqE2Atw9OncJsi0gGNw5k2Ay5u3xfCJlVZKPSDjKgqWIOpVYDVFqFN6IFu7qIgX\"}\n"
  },
  "app/production/server/privacy": {
    "principal": "NUgvbUViQy1hdWEwTG1cRGkkMjxVWD4xaXdZT3R5YTF6X0JoMDlMQUpiZ2kuNnVwbDNWXDhQQmFjUlRkLUdwZg=="
  },
  "app/production/vendor/mailgun": {
    "apiKey": "1234567890"
  }
}

To be compliant with our internal secret organization (CSO), we need to rewrite the secret paths to append the product key prefix.

Let's create a BundlePatch to change all prefixes from app/{{.Env}} to app/{{.Env}}/security/sops-sample/v1.0.0/microservice-1.

apiVersion: harp.elastic.co/v1
kind: BundlePatch
meta:
  name: "secret-relocator"
  description: "Move sops secrets to CSO compliant path"
spec:
  rules:
  - selector:
      matchPath:
        regex: "^app/production/*"
    package:
      path:
        template: |-
            app/production/security/sops-sample/v1.0.0/microservice-1/{{ trimPrefix "app/production/" .Path }}
  - selector:
      matchPath:
        regex: "^app/staging/*"
    package:
      path:
        template: |-
            app/staging/security/sops-sample/v1.0.0/microservice-1/{{ trimPrefix "app/staging/" .Path }}

Run the package relocation :

$ harp from object --on production.yaml \
  | harp bundle patch --spec cso-relocator.yaml \
  | harp bundle dump --data-only \
  | jq
{
  "app/production/security/sops-sample/v1.0.0/microservice-1/database": {
    "password": "c7#kaTKgAqjAuC6KKdAz)_tsa9mBetD83lAk2cSvyH.jB0AeW%/m4g~4Vi]|I7x!",
    "user": "app-xhyXgOEzYaPmFtYS"
  },
  "app/production/security/sops-sample/v1.0.0/microservice-1/server/http/session": {
    "cookieKeyB64": "fHJ1N1Z6YWI5RSIuWm1VNHZCTVB4NGdjSCVmJSFseWNwcjVRT2gwWC95TSlmNVVPfTc5X2tmaHp4TVUxSHR0dQ=="
  },
  "app/production/security/sops-sample/v1.0.0/microservice-1/server/http/token": {
    "signingPrivateKeyJWK": "{\"kty\":\"EC\",\"kid\":\"m5xr81U_PIKPLd0uzi6QH_kefkMoiN3fF5OUR34cF5I=\",\"crv\":\"P-384\",\"x\":\"nqrvtZkR8WxH_1URu3rDkd-FtBysDSjfmyUwctEV5xpWH_O71rOCsZDXltB5wBtq\",\"y\":\"0tqE2Atw9OncJsi0gGNw5k2Ay5u3xfCJlVZKPSDjKgqWIOpVYDVFqFN6IFu7qIgX\",\"d\":\"Ln4mPosjcCXRXq7TcKEVoWS8b2kfpCXnT4ifUSFFtAzesLWKo9AUQg1lStt8Oz8P\"}\n",
    "signingPublicKeyJWK": "{\"kty\":\"EC\",\"kid\":\"m5xr81U_PIKPLd0uzi6QH_kefkMoiN3fF5OUR34cF5I=\",\"crv\":\"P-384\",\"x\":\"nqrvtZkR8WxH_1URu3rDkd-FtBysDSjfmyUwctEV5xpWH_O71rOCsZDXltB5wBtq\",\"y\":\"0tqE2Atw9OncJsi0gGNw5k2Ay5u3xfCJlVZKPSDjKgqWIOpVYDVFqFN6IFu7qIgX\"}\n"
  },
  "app/production/security/sops-sample/v1.0.0/microservice-1/server/privacy": {
    "principal": "NUgvbUViQy1hdWEwTG1cRGkkMjxVWD4xaXdZT3R5YTF6X0JoMDlMQUpiZ2kuNnVwbDNWXDhQQmFjUlRkLUdwZg=="
  },
  "app/production/security/sops-sample/v1.0.0/microservice-1/vendor/mailgun": {
    "apiKey": "1234567890"
  }
}

Once everything is compliant with your expectations, publish to Vault.

$ harp from object --in production.yaml \
  | harp bundle patch --spec cso-relocator.yaml \
  | harp to vault

Application access profile

Secrets are now published in Vault by the operator. We need to create a secret consumer role and a bound policy to restrict operations on secrets.

Start by defining the Application role first.

apiVersion: harp.elastic.co/terraformer/v1
kind: AppRoleDefinition
meta:
  name: "sample"
  owner: "cloud-security@elastic.co"
  description: "sample service approle & policy"
  issues:
    - <github issue urls attached to the access request>
spec:
  selector:
    platform: "security"
    product: "sample"
    version: "v1.0.0"
    component: "microservice-1"
    environments:
      - production
      - staging

  namespaces:
    # CSO Compliant paths
    application:
      - suffix: "database"
        description: "Database connnection settings"
        capabilities: ["read"]
      - suffix: "server/privacy"
        description: "Privacy anonymizer"
        capabilities: ["read"]
      - suffix: "server/session"
        description: "HTTP Session related secrets"
        capabilities: ["read"]
      - suffix: "server/token"
        description: "JWT Token provider related secrets"
        capabilities: ["read"]
      - suffix: "vendor/mailgun"
        description: "Mailgun vendor"
        capabilities: ["read"]

Generate the service Vault AppRole and Policy using terraform script via harp-terraformer plugin.

$ harp terraformer service --spec approle.yaml

It will generate the following HCL script

# Generated with Harp Terraformer, Don't modify.
# https://github.com/zntrio/harp-plugins/tree/main/cmd/harp-terraformer
# ---
# SpecificationHash: "8nuVOhyalizDvImmlIAoCqH91OBIPUhL4ab24bpUdNE="
# Owner: "cloud-security@elastic.co"
# Date: "2021-08-09T11:14:34Z"
# Description: "sample service approle & policy"
# Issues:
# ---
#
# ------------------------------------------------------------------------------

# Create the policy
data "vault_policy_document" "service-sample-production" {
  # Application secrets
  rule {
    description  = "Database connnection settings"
    path         = "app/data/production/security/sample/v1.0.0/microservice-1/database"
    capabilities = ["read"]
  }

  rule {
    description  = "Privacy anonymizer"
    path         = "app/data/production/security/sample/v1.0.0/microservice-1/server/privacy"
    capabilities = ["read"]
  }

  rule {
    description  = "HTTP Session related secrets"
    path         = "app/data/production/security/sample/v1.0.0/microservice-1/server/session"
    capabilities = ["read"]
  }

  rule {
    description  = "JWT Token provider related secrets"
    path         = "app/data/production/security/sample/v1.0.0/microservice-1/server/token"
    capabilities = ["read"]
  }

  rule {
    description  = "Mailgun vendor"
    path         = "app/data/production/security/sample/v1.0.0/microservice-1/vendor/mailgun"
    capabilities = ["read"]
  }
}

# Register the policy
resource "vault_policy" "service-sample-production" {
  name   = "service-sample-production"
  policy = data.vault_policy_document.service-sample-production.hcl
}

# ------------------------------------------------------------------------------
#
# Register the backend role
resource "vault_approle_auth_backend_role" "sample-production" {
  backend   = "service"
  role_name = "sample-production"

  token_policies = [
    "service-default",
    "service-sample-production",
  ]
}

Mozilla SOPS

Mozilla SOPS is a file-encryption based solution which allows you to handle multi-recipient encryption where keys can be handled by Cloud KMS, PGP, Age, etc.

As a sample, I decided to use age encryption as an alternative to PGP.

Create the identity

We need to generate a secret key pair. Using age-keygen.

$ age-keygen -o sample-production.age
Public key: age18nnjwxyu4v45ma3dp40n42qfzhlxz5fuwuxlmkcf8x625wusze0s0qy4ug

This file is sensitive and must be kept secret.

# created: 2021-08-10T11:46:29+02:00
# public key: age18nnjwxyu4v45ma3dp40n42qfzhlxz5fuwuxlmkcf8x625wusze0s0qy4ug
AGE-SECRET-KEY-13HA98GJ0NGZY7KCHQVCM5SYZG9EJ8VR0UJW4FW0NWSXY49T280TSWUM7MG

Seal using sops

Encrypt and replace file content using the age public key. So that only the owner of the private key could decrypt the file.

$ sops -e -i \
    --age age18nnjwxyu4v45ma3dp40n42qfzhlxz5fuwuxlmkcf8x625wusze0s0qy4ug \
    production.yaml

Now you can see that the production.yaml file is encrypted, but it keeps its structure visible.

app:
    production:
        database:
            user: ENC[AES256_GCM,data:oQRwVndIF6nEdok1NwjaQLNjF64=,iv:+20uH+6df96a3a9cqIH8WCKjPyQzKkr7TmnCVAey8A0=,tag:0zjvnOldE/wP8SGBOXoDaw==,type:str]
            password: ENC[AES256_GCM,data:QfNWOZajBhqBlCYTf/7+0xRMsA6b8kdF8Z5B6wBDHMI0qOBf6OvAgMlwa2In1jTOs/LZtQA/USVf4VijyyVeug==,iv:Y0UKXVwPglJBrfN9jD0EHHI7VUl4lJwu4kd7DbzsLX4=,tag:MB/tZFpt8iARmDC8SUfl0g==,type:str]
        server:
            privacy:
                principal: ENC[AES256_GCM,data:sU//BpXSuCslRI0QEWn7OD40kvvbuer/E44ahKL1uq833g3rpAlH56VMnX8JIWe2HEjFPbcW73Ln2NznhWInIMeQw2Z8LbchVxLZKw8dvaJfuP2hNN9x4A==,iv:32mMVST6ssYrFw357+vJt86alY8D6u6+EQOP7i0lnp0=,tag:6qdFYygrMB0ZY3VIO6i3Mw==,type:str]
            http:
                session:
                    cookieKeyB64: ENC[AES256_GCM,data:hNQ6CEpwIPH+/7S1QcmVRn9ID+Bl49BzA1kKT7EjhuwD4coWzH0i1gGCE1GlGjZBqY2g0Kr6zuD/n38x5xsAX/CWa8JO2Nakw5lqPK0R00bZtnRpi2m1Gw==,iv:mUqwnfQ1f4EfoC0zLWNZ1rkfgLykWsUfDbdC+IHijls=,tag:GxSl7iDPOGDuRSfG9Hy7OQ==,type:str]
                token:
                    signingPrivateKeyJWK: ENC[AES256_GCM,data:vYw8oXa1PiFyVpQi63VdrcxZLY9fHWp5aImi2MF61j5zwNPzTrjEWfyFTElEieppb6BLiFRhfa2xh8jq1FIxzzhnkcQkjTGfOPRgON6si2Oxy94m8iPWiZFFQticPShSCTyOjion9KAhxCjwSHJ0YLc5DJ7Xab8/LLNhz+UvT3O8fnw0lSk2e+qcn7TcN6a9EM73LTFYnneOjRqKC3p8zMKg7wmlHiPKTabd0Vr1jYWRURetdmb5km2ee8YXJ7qziyPyJGn13QHfUcOoCgrgnmRVuWEgeB/7g17Dg7lLwyQPW2BnUTvBiaowjUSkUGs5NhybLx7wlHBX6kcKWm7aOyH+3Ma80Gyi4jn/Lwap1UjhBMEZkMRV3zSrPt6Kqc956k2vNA==,iv:FMitt6BsTuyQLhOtrvHl51paNvwgFRromyK6HMnasAU=,tag:gFswivMyMMZdB3lx6PEJzw==,type:str]
                    signingPublicKeyJWK: ENC[AES256_GCM,data:G4vXSZ+oEdp3T3y3x5eRfhg4bwqpwaAeZlrB56nAer2it3ptkhwaTONp2SndmZDB5vMzNo8ARMTYhpVfdJfvSCxf+6ovJ2PCW0pcYhVS4ynEUgkZWFDnlqFDv0oQd3t6CbIN4ZeO/XX5k8w0TK+HCV54m4nQaywLHnm1b+qtH1sssO5OXRKeSRqC3VZe7UaiTFLorkkzdGBu/JKTi5iPTAxrNWRJ5le0+FuENNUohMU13Sih2D8SGzmthSLFvr6ntbYgwpbyj4IiObS/35TGcDf1cMNzztvZ6JbDPDM=,iv:13Tn46zhwt4sMt8QpvDiZzCMgJFBgSzB+gIbPZcgZs0=,tag:API7I+Mg6WEbpJIzhaZ/Wg==,type:str]
        vendor:
            mailgun:
                apiKey: ENC[AES256_GCM,data:7RkPURpjiMu8tw==,iv:MOYpKEGiazFrTx7w1tbjUcVrRas4ckIKB6fhoLB4TEQ=,tag:XTZgMu9hriG9mavmo7gdZA==,type:str]
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age:
        - recipient: age18nnjwxyu4v45ma3dp40n42qfzhlxz5fuwuxlmkcf8x625wusze0s0qy4ug
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnQlFnQ1lDdGZQVzJGTVBW
            TzJycE9zZk53MGU3Uk9sY3gzR0lLZnlidkdBCkRPZjNOcThQT2VVckJLZ3BCay9V
            cVVxSndwUXRYdjU2cFJ0YWdWT2llY28KLS0tIFdZbE1FOW13SHd1UWpVMXpSR2Fj
            eUNzRXNnbTc4dzNXVFhGeGpXcU9KaEEKes529R7EtlY6IMHVpCjeL0Wk9Aj2r0ch
            Wi3NVJSKrsHKsIPdwCMkvFXP9dkWRfvowARZqCTQy18aITUhfwdN6g==
            -----END AGE ENCRYPTED FILE-----
    lastmodified: "2021-08-10T11:43:30Z"
    mac: ENC[AES256_GCM,data:GLVSyoSeWm2obzgl/8HXNx9nKsw7FiIyp6ypQpEXZsmD/quysoMphviv6AoLfwu8u2CUUyi8Eycd9bCSP6bvbkihVT1rsH42wvNatPjm/YwdK/CYF1mrveLskwlWAYEqvFjd4ls+IZh63N7fP5q5waUaDdPY2H21vKcpULujdZQ=,iv:YO7qZKNqR1M8K+vV4DqBKXA69MPj8xhlgr+pQCr12Qk=,tag:drXuAtMSyYm3nEUylO8CVA==,type:str]
    pgp: []
    unencrypted_suffix: _unencrypted
    version: 3.7.1

You can now safely publish production.yaml file to Git

$ git add production.yaml
$ git commit -a -m "chore(secrets): update production secrets."
$ git push

Retrieve a bundle from a sops encrypted file

You can decrypt the production.yaml file if you have access to the private key used to encrypt.

$ export SOPS_AGE_KEY_FILE=sample-production.age
$ sops -d production.yaml
app:
    production:
        database:
            user: app-tNtKnOgYXuuJyArI
            password: (Pzd~O9JQ"Epm)QN-eWZAscJHxjyfQlXXq_t=HwEc1it3Bu9h+A7um777q"t4n*8
        server:
            privacy:
                principal: R0FLZVROZDZlMGRUPFYsQXJRWFluVGlJYUFCTDBIT3p5a145TTRAZjNsc0B6XnhmKGo0djU4Y2AzY01vaDolcA==
            http:
                session:
                    cookieKeyB64: aV5kU292VTYvTlhJM1BrdDgzSEJUcXgrOSVIfkU2Tlp2azN4YXBhWlEpLmJjZiNkZz96VyVvQ0piN3YwbHRpNQ==
                token:
                    signingPrivateKeyJWK: '{"kty":"EC","kid":"sAt9gTz5oUmTefVK0Ib1OfRv7uKVC1at9bB1VjR52QE=","crv":"P-384","x":"XeCZRaJDh4i1ywansUMAh2kN6WbEqWNnQc0diC0SkVxmCAcxA69PQKbyYJy49z9Y","y":"FuDWJpFuuYX0JDRSJTQ5uexLCU3-G4tGEGAoRtWQLRCZIpz4tfcd87bFiIhAb4MT","d":"gHmrbNC5X8rp4ORxwQkdeqMctMjP_jLO3ngd_oxp-mclK3O7TIpPM0dm7TEx0AjR"}'
                    signingPublicKeyJWK: '{"kty":"EC","kid":"sAt9gTz5oUmTefVK0Ib1OfRv7uKVC1at9bB1VjR52QE=","crv":"P-384","x":"XeCZRaJDh4i1ywansUMAh2kN6WbEqWNnQc0diC0SkVxmCAcxA69PQKbyYJy49z9Y","y":"FuDWJpFuuYX0JDRSJTQ5uexLCU3-G4tGEGAoRtWQLRCZIpz4tfcd87bFiIhAb4MT"}'
        vendor:
            mailgun:
                apiKey: "1234567890"

In order to convert a YAML object to a Harp Secret Bundle, you have to use harp from object.

$ sops -d production.yaml \
  | harp from object \
  | harp bundle dump --data-only \
  | jq
{
  "app/production/database": {
    "password": "(Pzd~O9JQ\"Epm)QN-eWZAscJHxjyfQlXXq_t=HwEc1it3Bu9h+A7um777q\"t4n*8",
    "user": "app-tNtKnOgYXuuJyArI"
  },
  "app/production/server/http/session": {
    "cookieKeyB64": "aV5kU292VTYvTlhJM1BrdDgzSEJUcXgrOSVIfkU2Tlp2azN4YXBhWlEpLmJjZiNkZz96VyVvQ0piN3YwbHRpNQ=="
  },
  "app/production/server/http/token": {
    "signingPrivateKeyJWK": "{\"kty\":\"EC\",\"kid\":\"sAt9gTz5oUmTefVK0Ib1OfRv7uKVC1at9bB1VjR52QE=\",\"crv\":\"P-384\",\"x\":\"XeCZRaJDh4i1ywansUMAh2kN6WbEqWNnQc0diC0SkVxmCAcxA69PQKbyYJy49z9Y\",\"y\":\"FuDWJpFuuYX0JDRSJTQ5uexLCU3-G4tGEGAoRtWQLRCZIpz4tfcd87bFiIhAb4MT\",\"d\":\"gHmrbNC5X8rp4ORxwQkdeqMctMjP_jLO3ngd_oxp-mclK3O7TIpPM0dm7TEx0AjR\"}",
    "signingPublicKeyJWK": "{\"kty\":\"EC\",\"kid\":\"sAt9gTz5oUmTefVK0Ib1OfRv7uKVC1at9bB1VjR52QE=\",\"crv\":\"P-384\",\"x\":\"XeCZRaJDh4i1ywansUMAh2kN6WbEqWNnQc0diC0SkVxmCAcxA69PQKbyYJy49z9Y\",\"y\":\"FuDWJpFuuYX0JDRSJTQ5uexLCU3-G4tGEGAoRtWQLRCZIpz4tfcd87bFiIhAb4MT\"}"
  },
  "app/production/server/privacy": {
    "principal": "R0FLZVROZDZlMGRUPFYsQXJRWFluVGlJYUFCTDBIT3p5a145TTRAZjNsc0B6XnhmKGo0djU4Y2AzY01vaDolcA=="
  },
  "app/production/vendor/mailgun": {
    "apiKey": "1234567890"
  }
}

From this point, you can apply all harp available bundle operations.

Generate a YAML output from a bundle

For example to have applied a BundlePatch to rotate a secret value.

apiVersion: harp.elastic.co/v1
kind: BundlePatch
meta:
  name: "token-jwk-rotator"
  description: "Create a new JWK key for JWT signing."
spec:
  rules:
  - selector:
      matchPath:
        strict: "app/production/server/http/token"
    package:
        data:
            template: |-
                {
                    "signingPrivateKeyJWK": {{ $key := cryptoPair "ec:p384" }}{{ $key.Private | toJwk | toJson }},
                    "signingPublicKeyJWK": {{ $key.Public | toJwk | toJson }}
                }

This patch will look for app/production/server/http/token package in the given bundle and generate 2 secrets signingPrivateKeyJWK and signingPublicKeyJWK which will replace the old values.

$ sops -d production.yaml \
  | harp from object \
  | harp bundle patch --spec token-jwk-rotator.yaml --out rotated.bundle
$ harp bundle dump --in rotated.bundle --data-only \
  | jq
{
  ...
  "app/production/server/http/token": {
    "signingPrivateKeyJWK": "{\"kty\":\"EC\",\"kid\":\"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=\",\"crv\":\"P-384\",\"x\":\"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7\",\"y\":\"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d\",\"d\":\"TE0i2Ry0Dailcs2FczPsOapePqh85uSV538M_iUNJlpkorguLpauMnxY4zSBzohd\"}",
    "signingPublicKeyJWK": "{\"kty\":\"EC\",\"kid\":\"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=\",\"crv\":\"P-384\",\"x\":\"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7\",\"y\":\"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d\"}"
  },
  ...
}

Check the updated secrets

$ sops -d production.yaml \
  | harp from object \
  | harp bundle diff --old - --new rotated.bundle \
  | jq
[
  {
    "op": "replace",
    "type": "secret",
    "path": "app/production/server/http/token#signingPrivateKeyJWK",
    "value": "{\"kty\":\"EC\",\"kid\":\"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=\",\"crv\":\"P-384\",\"x\":\"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7\",\"y\":\"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d\",\"d\":\"TE0i2Ry0Dailcs2FczPsOapePqh85uSV538M_iUNJlpkorguLpauMnxY4zSBzohd\"}"
  },
  {
    "op": "replace",
    "type": "secret",
    "path": "app/production/server/http/token#signingPublicKeyJWK",
    "value": "{\"kty\":\"EC\",\"kid\":\"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=\",\"crv\":\"P-384\",\"x\":\"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7\",\"y\":\"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d\"}"
  }
]

Regenerate the YAML output for SOPS :

$ harp to object --in rotated.bundle \
    --format yaml

Generate the YAML flattened secret tree :

app/production/database:
    password: (Pzd~O9JQ"Epm)QN-eWZAscJHxjyfQlXXq_t=HwEc1it3Bu9h+A7um777q"t4n*8
    user: app-tNtKnOgYXuuJyArI
app/production/server/http/session:
    cookieKeyB64: aV5kU292VTYvTlhJM1BrdDgzSEJUcXgrOSVIfkU2Tlp2azN4YXBhWlEpLmJjZiNkZz96VyVvQ0piN3YwbHRpNQ==
app/production/server/http/token:
    signingPrivateKeyJWK: '{"kty":"EC","kid":"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=","crv":"P-384","x":"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7","y":"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d","d":"TE0i2Ry0Dailcs2FczPsOapePqh85uSV538M_iUNJlpkorguLpauMnxY4zSBzohd"}'
    signingPublicKeyJWK: '{"kty":"EC","kid":"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=","crv":"P-384","x":"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7","y":"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d"}'
app/production/server/privacy:
    principal: R0FLZVROZDZlMGRUPFYsQXJRWFluVGlJYUFCTDBIT3p5a145TTRAZjNsc0B6XnhmKGo0djU4Y2AzY01vaDolcA==
app/production/vendor/mailgun:
    apiKey: "1234567890"

Use --expand to regenerate the expanded secret tree :

$ harp to object --in rotated.bundle \
    --format yaml \
    --expand
app:
    production:
        database:
            password: (Pzd~O9JQ"Epm)QN-eWZAscJHxjyfQlXXq_t=HwEc1it3Bu9h+A7um777q"t4n*8
            user: app-tNtKnOgYXuuJyArI
        server:
            http:
                session:
                    cookieKeyB64: aV5kU292VTYvTlhJM1BrdDgzSEJUcXgrOSVIfkU2Tlp2azN4YXBhWlEpLmJjZiNkZz96VyVvQ0piN3YwbHRpNQ==
                token:
                    signingPrivateKeyJWK: '{"kty":"EC","kid":"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=","crv":"P-384","x":"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7","y":"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d","d":"TE0i2Ry0Dailcs2FczPsOapePqh85uSV538M_iUNJlpkorguLpauMnxY4zSBzohd"}'
                    signingPublicKeyJWK: '{"kty":"EC","kid":"IM_00Nm7zruMnBTxumkzT-LNiOnfGWStjznFm5diRVc=","crv":"P-384","x":"sH6CbEV9HqgSAxCdjjBOPgdw1xvAcMrNaOl2Vkrq9x6LfJKxEA1qioTa_1zAYbJ7","y":"5iOnCQRyXHUPzg8RL2PV_-jdKTUpzH4v1QPBAltEdyJ_4rwJwT5oFdNm_Uqqpy2d"}'
            privacy:
                principal: R0FLZVROZDZlMGRUPFYsQXJRWFluVGlJYUFCTDBIT3p5a145TTRAZjNsc0B6XnhmKGo0djU4Y2AzY01vaDolcA==
        vendor:
            mailgun:
                apiKey: "1234567890"

Save the output to a file, apply sops encryption and git commit/push :

$ harp to object --in rotated.bundle \
    --format yaml \
    --expand \
    --out production.yaml
$ sops -e -i \
    --age age18nnjwxyu4v45ma3dp40n42qfzhlxz5fuwuxlmkcf8x625wusze0s0qy4ug \
    production.yaml
$ git add production.yaml
$ git commit -a -m "chore(secrets): update production secrets."
$ git push

Complete scenario

# Retrieve most recent secret cold state
$ git pull
# Decrypt and retrieve actual secret state
$ sops -d production.yaml \
  | harp from object \
  | harp bundle patch --spec token-jwk-rotator.yaml --out rotated.bundle
# Publish changes to Vault (can be done via GitOps after push)
$ harp bundle patch --spec cso-relocator.yaml --in rotated.bundle \
  | harp to vault
# Publish changes to SOPS
$ harp to object --in rotated.bundle \
    --format yaml \
    --expand \
    --out production.yaml
# Encrypt the production secrets
$ sops -e -i \
    --age $CI_PUBLIC_KEY \
    production.yaml
# Cleanup
$ rm -f rotated.bundle
# Push them to git
$ git add production.yaml
$ git commit -a -m "chore(ci): secrets/sample - auto-rotate JWT Token signing key."
$ git push