A BundlePatch
allows secret operators to describe Bundle
modification and
make them reproducible. Each BundlePatch
generate a new Bundle
form the
bundle source without altering the source bundle.
- BundlePatch
PatchOperation
holds information used to alter a key/value formatted object.
add
is used to add a new (key => value) association in the mapremove
is used to remove a key from the mapupdate
is used to update a value from an existingkey
onlyreplaceKeys
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;
}
kv:
create:
new-key-1: value1
# With template
new-key-{{.Values.number}}: {{ strongPassword | toYaml }}
kv:
update:
key-1: new-value
kv:
remove:
- key-1
- key-2
kv:
removeKeys:
- "key-[0-9]+"
kv:
replaceKeys:
"old-key": "new-key"
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;
}
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 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;
}
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"
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;
}
selector:
matchPath:
strict: product/harp/v1.0.0/artifacts/attestations/cosign/private_key
selector:
matchPath:
regex: ^app/production/.*$
selector:
jmesPath: labels.database == "postgres"
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
selector:
regoFile: deprecation.rego
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
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
is a package path matcher.
strict
is used to filter package path when strictly equal to the given valueregex
is used to match the package path with the given regular expressionglob
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
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;
}
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 }}
selector:
matchPath:
regex: "^services/production/global/clusters/*"
package:
path:
template: |-
app/production/global/clusters/1.0.0/bootstrap/{{ trimPrefix "services/production/global/observability/" .Path }}
selector:
jmesFilter: labels.deprecated == true
package:
remove: true
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 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;
}
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"
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"
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"
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
$ 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