Skip to content

Latest commit

 

History

History
620 lines (516 loc) · 14.1 KB

4-patch.md

File metadata and controls

620 lines (516 loc) · 14.1 KB

BundlePatch

A BundlePatch allows secret operators to describe Bundle modification and make them reproducible. Each BundlePatchgenerate a new Bundle form the bundle source without altering the source bundle.

Specification

PatchOperation

PatchOperation holds information used to alter a key/value formatted object.

  • add is used to add a new (key => value) association in the map
  • remove is used to remove a key from the map
  • update is used to update a value from an existing key only
  • replaceKeys is used to rename a key to another key in the map.
  • removeKeys is used to remove all keys that match one of the given regex patterns.

All keys and values can contain template instructions.

// PatchOperation represents atomic patch operations executable on a k/v map.
message PatchOperation {
  // Add a new case-sentitive key and value to related data map.
  // Key and Value can be templatized.
  map<string,string> add = 1;
  // Remove a case-sensitive key from related data map.
  // Key and Value can be templatized.
  repeated string remove = 2;
  // Update case-sensitive existing key from related data map.
  // Key and Value can be templatized.
  map<string,string> update = 3;
  // Replace case-sensitive existing key using the associated value.
  // Value can be templatized.
  map<string,string> replaceKeys = 4;
  // Remove all keys matching these given regexp.
  repeated string removeKeys = 5;
}

Create a key ⇒ value

kv:
  create:
    new-key-1: value1
    # With template
    new-key-{{.Values.number}}: {{ strongPassword | toYaml }}

Update values

kv:
  update:
    key-1: new-value

Remove a key

kv:
  remove:
    - key-1
    - key-2

Remove keys by pattern

kv:
  removeKeys:
    - "key-[0-9]+"

Replace keys

kv:
  replaceKeys:
    "old-key": "new-key"

PatchSpec

PatchSpec defines the ordered PatchRule collection to apply during the Bundle transformation.

// PatchSpec repesetns bundle patch specification holder.
message PatchSpec {
  // Patch selector rules. Applied in the declaration order.
  repeated PatchRule rules = 1;
}

Sample

apiVersion: harp.elastic.co/v1
kind: BundlePatch
meta:
  name: patch-name
  description: Patch description to help
spec:
  rules:
  # Rule 1
  - selector: ...
  # Rule 2
  - selector: ...

PatchRule

// PatchRule represents an operation to apply to a given bundle.
message PatchRule {
  // Used to determine is patch strategy is applicable to the package.
  PatchSelector selector = 1;
  // Package patch operations.
  PatchPackage package = 2;
}

Sample

rules:
  # Define who is eligible to the PatchOperation
  - selector:
      matchPath:
        strict: product/harp/v1.0.0/artifacts/attestations/cosign/private_key
    package:
      data:
        # Alter package secrets
        kv:
          replaceKeys:
            "key": "COSIGN_PRIVATE_KEY"
            "password": "COSIGN_PRIVATE_KEY_PASSWORD"

PatchSelector

PathSelector defines the strategy used to mark the package as eligible to PatchOperation execution.

// PatchSelector represents selecting strategies used to match a bundle resource.
message PatchSelector {
  // Match a package by using its path (secret path).
  PatchSelectorMatchPath matchPath = 1;
  // Match a package using a JMESPath query.
  string jmesPath = 2;
  // Match a package using a Rego policy.
  string rego = 3;
  // Match a package using a REgo policy stored in an external file.
  string regoFile = 4;
  // Match a package by secret.
  PatchSelectorMatchSecret matchSecret = 5;
  // Match a package using CEL expressions.
  repeated string cel = 6;
}

Match by strict path

selector:
  matchPath:
    strict: product/harp/v1.0.0/artifacts/attestations/cosign/private_key

Match by regex path

selector:
  matchPath:
    regex: ^app/production/.*$

Match by JMES filter

selector:
  jmesPath: labels.database == "postgres"

Match by Rego policy

selector:
  rego: |-
    package harp
    default matched = false
    matched {
        input.annotations["infosec.elastic.co/v1/SecretPolicy#severity"] == "moderate"
        input.secrets.data[_].key == "cookieEncryptionKey"
    }

Sample use case

apiVersion: harp.elastic.co/v1
kind: BundlePatch
meta:
  name: "package-secret-rotation-flagger"
  owner: [email protected]
  description: "Flag deprecated packages"
spec:
  rules:
    - selector:
        # https://play.openpolicyagent.org/p/lXEVMXpmvi
        rego: |-
            package harp

            # Default decision
            default matched = false

            # Constants
            annotationGenerationDate = "secrets.elastic.co/generationDate"
            annotationRotationPeriod = "secrets.elastic.co/rotationPeriod"

            # ----------------------------------------------------------------

            matched {
                has_rotation_annotations
                must_rotate
            }

            # Helpers --------------------------------------------------------

            # Check annotations presence
            has_rotation_annotations {
                input.annotations[annotationGenerationDate]
                input.annotations[annotationRotationPeriod]
            }

            # Determine if the secret must be rotated
            must_rotate {
                genDate := time.parse_rfc3339_ns(input.annotations[annotationGenerationDate])
                rotationPeriod := to_number(input.annotations[annotationRotationPeriod])
                time.add_date(genDate, 0, 0, rotationPeriod) < time.now_ns()
            }

      package:
        labels:
            add:
                deprecated: true

Match by Rego policy file

selector:
  regoFile: deprecation.rego

Match by CEL expression

selector:
  cel:
    - "p.is_cso_compliant()"

Sample use case

apiVersion: harp.elastic.co/v1
kind: BundlePatch
meta:
  name: "cso-compliance-flagger"
  owner: [email protected]
  description: "Flag non CSO complaint packages"
spec:
  rules:
    - selector:
        cel:
          # No CSO compliant path as package name
          - "!p.is_cso_compliant()"
      package:
        labels:
          add:
            cso-compliant: false
    - selector:
        cel:
          # CSO compliant path as package name
          - "p.is_cso_compliant()"
      package:
        labels:
          add:
            cso-compliant: true

Match by secret key

Strict matcher

selector:
  matchSecret:
    strict: USER

Regex matcher

selector:
  matchSecret:
    regex: "*_KEY"

Complete sample

apiVersion: harp.elastic.co/v1
kind: BundlePatch
meta:
  name: "secret-remover"
  owner: [email protected]
  description: "Remove a targeted secrets"
spec:
  rules:
    - selector:
        matchSecret:
          strict: USER
      package:
        data:
          kv:
            remove:
              - USER

PatchSelectorMatchPath

PatchSelectorMatchPath is a package path matcher.

  • strict is used to filter package path when strictly equal to the given value
  • regex is used to match the package path with the given regular expression
  • glob is used to match the package path with the given glob expression
// PatchSelectorMatchPath represents package path matching strategies.
message PatchSelectorMatchPath {
  // Strict case-sensitive path matching.
  // Value can be templatized.
  string strict = 1;
  // Regex path matching.
  // Value can be templatized.
  string regex = 2;
  // Glob path matching. - https://github.com/gobwas/glob
  // Value can be templatized.
  string glob = 3;
}

PatchPackage

PatchPackage represents operation applicable to the selected package.

// PatchPackage represents package operations.
message PatchPackage {
  // Path operations.
  PatchPackagePath path = 1;
  // Annotation operations.
  PatchOperation annotations = 2;
  // Label operations.
  PatchOperation labels = 3;
  // Secret data operations.
  PatchSecret data = 4;
  // Flag as remove.
  bool remove = 5;
  // Flag to create if not exist.
  bool create = 6;
}

PatchPackagePath

This is used to rename the current package to another.

  • template is used to define the new package path

The template exposes .Path as context value to retrieve the current value of the package.

// PatchPackagePath represents package path operations.
message PatchPackagePath {
  // Template used to completely rewrite the package path.
  string template = 1;
}

Sample

template: |-
    app/production/global/clusters/1.0.0/bootstrap/{{ trimPrefix "services/production/global/observability/" .Path }}

Rename a package

selector:
  matchPath:
    regex: "^services/production/global/clusters/*"
package:
  path:
    template: |-
        app/production/global/clusters/1.0.0/bootstrap/{{ trimPrefix "services/production/global/observability/" .Path }}

Remove a package

selector:
  jmesFilter: labels.deprecated == true
package:
  remove: true

Create if not exists

Package creation flag only works with strict path matcher.

selector:
  path:
    strict: infra/aws/security/eu-central-1/ec2/keys/ssh/ed25519_keys
  package:
    create: true
    data:
      template: |-
        {
          "private": {{ $sshKey := cryptoPair "ssh" }}{{ $sshKey.Private | toSSH | toJson }},
          "public": "{{ $sshKey.Public | toSSH | trim }} [email protected]"
        }

PatchSecret

// PatchSecret represents secret data operations.
message PatchSecret {
  // Secret data annotation operations.
  PatchOperation annotations = 1;
  // Secret data label operations.
  PatchOperation labels = 2;
  // Template to override secret data.
  string template = 3;
  // Used to target specific keys inside the secret data.
  PatchOperation kv = 4;
}

Alter annotations

selector:
  matchPath:
    regex: "^/database/postgres/(*.)/admin_account$"
package:
  annotations:
    add:
      "infosec.elastic.co/v1/RiskManagement#leakSeverity": "critical"
    update:
      "secrets.elastic.co/v1/SecretManagement#lastRotation": {{ now | date "2006-01-02T03:04:05Z" }}
    remove:
      - "secrets.elastic.co/{{.Values.schemaVersion}}/legacyAnnotation"
    replaceKeys:
      "old-annotation-key": "new-annotation-key"

Alter labels

selector:
  matchPath:
    regex: "^/database/postgres/(*.)/admin_account$"
package:
  labels:
    add:
      "database": "postgres"
    update:
      "database": "postgres-{{ .Values.postgresVersion }}"
    remove:
      - "database"
    replaceKeys:
      "old-label-key": "new-label-key"

Alter secret data

selector:
  matchPath:
    regex: "^/database/postgres/(*.)/admin_account$"
package:
  data:
    kv:
      add:
        "USER": "admin-{{ randAlpha 8 }}"
      update:
        "USER": "admin-{{ randAlpha 8 }}"
      remove:
        - "PASSWORD"
      replaceKeys:
        "old-secret-key": "new-secret-key"

Usage

Apply a patch

Given this patch postgresql-admin-rotator :

apiVersion: harp.elastic.co/v1
kind: BundlePatch
meta:
  name: "service-postgres-rotator"
  owner: [email protected]
  description: "Rotate all postgres service account password"
spec:
  rules:
  # Rule targets production and staging path
  - selector:
      matchPath:
        regex: "app/(production|staging)/{{.Values.environment}}/databases/posgresql/service_account"
    package:
      # Patch concerns secret data
      data:
        # We want to update a K/V couple
        kv:
          # Remove old keys (here generated username)
          removeKeys:
            - "admin-.*"
          # Update entry if exists
          update:
            "admin-{{ randAlpha 8 }}": "{{ strongPassword | b64enc }}"

Generate the input Bundle on which the BundlePatch will be applied :

$ harp from vault \
    --with-metadata \
    --paths app/production/security \
    # Apply the patch
    | harp bundle patch --spec service-postgres-rotator.yaml \
        --set environment=security \
    # Keep only target path
    | harp bundle filter \
        --keep app/production/security/databases/postgresql \
    # Create a new secret version in Vault
    | harp to vault

Generate a patch from bundle difference

$ harp from vault \
    --with-metadata \
    --paths app/production/security \
    --out initial.bundle
$ harp bundle patch --spec service-postgres-rotator.yaml \
    --set environment=security \
    --out patched.bundle
$ harp bundle diff \
    --old initial.bundle \
    --new patched.bundle \
    --patch