Skip to content

Commit dbdca9c

Browse files
committed
add ability to send encrypted secrets to disco backend
For now, this uses a hardcoded RSA key for which I threw away the private key, since we don't have the ability to pull JWKs yet This also includes a few test tweaks to help make this easier, and an example folder which produces an output.json showing how this can work Signed-off-by: Ashley Davis <ashley.davis@cyberark.com>
1 parent dfc7c39 commit dbdca9c

File tree

14 files changed

+534
-48
lines changed

14 files changed

+534
-48
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ predicate.json
1515

1616
_bin
1717
.envrc
18+
19+
examples/encrypted-secrets/output.json

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ go run . agent \
3434
> - [./agent.yaml](./agent.yaml).
3535
> - [./examples/one-shot-secret.yaml](./examples/one-shot-secret.yaml).
3636
> - [./examples/cert-manager-agent.yaml](./examples/cert-manager-agent.yaml).
37+
> - [./examples/encrypted-secrets](./examples/encrypted-secrets) - Send encrypted Kubernetes secrets to CyberArk.
3738
3839
You might also want to run a local echo server to monitor requests sent by the agent:
3940

deploy/charts/disco-agent/templates/deployment.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ spec:
7676
name: {{ .Values.authentication.secretName }}
7777
key: ARK_DISCOVERY_API
7878
optional: true
79+
- name: ARK_SEND_SECRETS
80+
value: {{ .Values.config.sendSecrets | default "false" | quote }}
7981
{{- with .Values.http_proxy }}
8082
- name: HTTP_PROXY
8183
value: {{ . }}

deploy/charts/disco-agent/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ config:
154154
# be communicated to the people responsible for the affected secrets.
155155
clusterDescription: ""
156156

157+
# Enable sending of Secret data to CyberArk, in addition to the metadata.
158+
# When enabled, Secret data is encrypted using envelope encryption using
159+
# a key managed by CyberArk.
160+
# Default: false (but default will change to true for a future release)
161+
sendSecrets: false
162+
157163
authentication:
158164
secretName: agent-credentials
159165

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Encrypted Secrets Example
2+
3+
This example demonstrates how to use the disco agent to gather Kubernetes secrets and encrypt their data fields.
4+
5+
## Overview
6+
7+
When the `ARK_SEND_SECRETS` environment variable is set to `"true"`, the disco agent will:
8+
9+
0. Fetch an encryption key from the configured endpoint (if running in production) or use a local key for testing
10+
1. Discover Kubernetes secrets in your cluster (excluding common system secret types)
11+
2. Encrypt each secret's data fields using RSA envelope encryption with JWE (JSON Web Encryption) format
12+
3. If running in production, send the encrypted secrets to the configured endpoint; otherwise, write them to `output.json` for testing
13+
14+
The encryption uses:
15+
16+
- **Key Algorithm**: RSA-OAEP-256 (for encrypting the content encryption key)
17+
- **Content Encryption**: AES-256-GCM (for encrypting the actual secret data)
18+
- **Format**: JWE Compact Serialization
19+
20+
Metadata (names, namespaces, labels, annotations) remains in plaintext for discovery purposes, while the sensitive secret data is encrypted. Some keys in Secret data fields are also preserved in the `data` section, for backwards compatibility.
21+
22+
## Prerequisites
23+
24+
1. A running Kubernetes cluster with secrets to discover
25+
3. Go installed
26+
27+
## Configuration File
28+
29+
The `config.yaml` file configures:
30+
31+
- The data gatherer to collect Kubernetes secrets
32+
- Field selectors to exclude system secrets (service account tokens, docker configs, etc.)
33+
- The cluster ID and organization ID for grouping data
34+
35+
## Running the Example
36+
37+
Test the agent locally by running this script:
38+
39+
```bash
40+
./test.sh
41+
```
42+
43+
This will:
44+
45+
- Connect to your current Kubernetes context
46+
- Gather all non-system secrets
47+
- Write the raw data to `output.json`
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# encrypted-secrets config.yaml
2+
#
3+
# An example configuration file demonstrating how to use the disco agent
4+
# to send encrypted secrets to CyberArk Discovery & Context.
5+
#
6+
# The agent will:
7+
# 1. Discover Kubernetes secrets in the cluster
8+
# 2. Encrypt the secret data fields using RSA envelope encryption (JWE format)
9+
# 3. Upload the encrypted secrets to CyberArk Discovery & Context
10+
#
11+
# Example usage:
12+
#
13+
# export ARK_SUBDOMAIN="your-subdomain"
14+
# export ARK_USERNAME="your-username"
15+
# export ARK_SECRET="your-secret"
16+
# export ARK_SEND_SECRETS="true"
17+
#
18+
# go run . agent \
19+
# --agent-config-file examples/encrypted-secrets/config.yaml \
20+
# --one-shot \
21+
# --output-path output.json
22+
#
23+
organization_id: "my-organization"
24+
cluster_id: "my_cluster"
25+
period: 1m
26+
data-gatherers:
27+
- kind: "k8s-dynamic"
28+
name: "k8s/secrets"
29+
config:
30+
resource-type:
31+
version: v1
32+
resource: secrets
33+
# Filter out common system secret types to focus on application secrets
34+
field-selectors:
35+
- type!=kubernetes.io/service-account-token
36+
- type!=kubernetes.io/dockercfg
37+
- type!=kubernetes.io/dockerconfigjson
38+
- type!=kubernetes.io/basic-auth
39+
- type!=kubernetes.io/ssh-auth
40+
- type!=bootstrap.kubernetes.io/token
41+
- type!=helm.sh/release.v1

examples/encrypted-secrets/test.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bash
2+
# test.sh - Test script for the encrypted secrets example
3+
#
4+
# This script demonstrates running the disco agent with encrypted secrets enabled.
5+
# It will run in one-shot mode and output to a local file for inspection.
6+
7+
set -euo pipefail
8+
9+
# Colors for output
10+
RED='\033[0;31m'
11+
GREEN='\033[0;32m'
12+
YELLOW='\033[1;33m'
13+
NC='\033[0m' # No Color
14+
15+
echo -e "${GREEN}=== Encrypted Secrets Example Test ===${NC}\n"
16+
17+
echo -e "${GREEN}Testing agent with Kubernetes secrets${NC}"
18+
echo ""
19+
20+
# Enable encrypted secrets
21+
export ARK_SEND_SECRETS="true"
22+
23+
# Check Kubernetes connectivity
24+
if ! kubectl cluster-info &> /dev/null; then
25+
echo -e "${RED}Error: Unable to connect to Kubernetes cluster${NC}"
26+
echo "Please ensure your kubeconfig is configured correctly."
27+
exit 1
28+
fi
29+
30+
echo -e "${GREEN}✓ Connected to Kubernetes cluster${NC}"
31+
CONTEXT=$(kubectl config current-context)
32+
echo " Context: ${CONTEXT}"
33+
echo ""
34+
35+
# Check for secrets
36+
SECRET_COUNT=$(kubectl get secrets --all-namespaces --no-headers 2>/dev/null | wc -l | tr -d ' ')
37+
echo "Found ${SECRET_COUNT} secrets in cluster"
38+
echo ""
39+
40+
# Run the agent in one-shot mode with output to file
41+
OUTPUT_FILE="output.json"
42+
echo -e "${GREEN}Running disco agent with encrypted secrets enabled...${NC}"
43+
echo "Command: go run ../.. agent --agent-config-file config.yaml --one-shot --output-path ${OUTPUT_FILE}"
44+
echo ""
45+
46+
if go run ../.. agent \
47+
--agent-config-file config.yaml \
48+
--one-shot \
49+
--output-path "${OUTPUT_FILE}"; then
50+
51+
echo ""
52+
echo -e "${GREEN}✓ Agent completed successfully${NC}"
53+
54+
# Check if output file was created
55+
if [ -f "${OUTPUT_FILE}" ]; then
56+
echo -e "${GREEN}✓ Output file created: ${OUTPUT_FILE}${NC}"
57+
else
58+
echo -e "${RED}✗ Output file was not created${NC}"
59+
exit 1
60+
fi
61+
else
62+
echo ""
63+
echo -e "${RED}✗ Agent failed${NC}"
64+
exit 1
65+
fi

internal/envelope/rsa/keys.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ import (
1010

1111
// This file contains helpers for loading keys. In practice we'll retrieve keys in some format from a DisCo endpoint
1212

13+
const (
14+
// HardcodedPublicKeyPEM contains a temporary hardcoded RSA public key (2048-bit) for envelope encryption.
15+
// This is a TEMPORARY solution for initial development and testing.
16+
// TODO: Replace with dynamic key fetching from CyberArk Discovery & Context API.
17+
HardcodedPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
18+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoeq+dk4aoGdV9xjrnGJt
19+
VbUh5jvkQgynkP+9Ph2NVeoasXWqYOmOVeKOI7Yr58W/L8Mro6C22iSEJrPFgPF6
20+
t+RJsLAsAY6w1Pocq16COeelAWtxhHQGXt77WQKk0kmwhOJZ4VSeiQC4hWLUnq4N
21+
Ft7lwLw/50opTXLuSErrwec/bEV7G/Xp11BMsHGEL7dzpwWAfIrbCEomyWrO/L6p
22+
O3SAgYMdfup5ddnszeCU2FbFQziOkuMLOyir91XXk8wgdSy4IGAEGpwNx88i8fuj
23+
Qafze2aGWUtpWlOEQPP8lH2cj2TGUgLxGITbczJRcwuGIoJBOzAmPDWi/bapj4b6
24+
zQIDAQAB
25+
-----END PUBLIC KEY-----`
26+
27+
// hardcodedUID is a temporary hardcoded UID associated with the hardcoded public key
28+
// It was randomly generated with the macOS "uuidgen" commmand
29+
hardcodedUID = "A39798E6-8CE7-4E6E-9CF6-24A3C923B3A7"
30+
)
31+
1332
// LoadPublicKeyFromPEM parses an RSA public key from PEM-encoded bytes.
1433
// The PEM block should be of type "PUBLIC KEY" or "RSA PUBLIC KEY".
1534
func LoadPublicKeyFromPEM(pemBytes []byte) (*rsa.PublicKey, error) {
@@ -55,3 +74,16 @@ func LoadPublicKeyFromPEMFile(path string) (*rsa.PublicKey, error) {
5574

5675
return LoadPublicKeyFromPEM(pemBytes)
5776
}
77+
78+
// LoadHardcodedPublicKey loads and parses the hardcoded RSA public key.
79+
// Returns a hardcoded UID associated with the key.
80+
// This is a temporary solution for initial development and testing.
81+
// Returns an error if the hardcoded key is invalid or cannot be parsed.
82+
func LoadHardcodedPublicKey() (*rsa.PublicKey, string, error) {
83+
key, err := LoadPublicKeyFromPEM([]byte(HardcodedPublicKeyPEM))
84+
if err != nil {
85+
return nil, "", err
86+
}
87+
88+
return key, hardcodedUID, nil
89+
}

internal/envelope/rsa/keys_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,24 @@ func TestLoadPublicKeyFromPEMFile_InvalidContent(t *testing.T) {
142142
require.Error(t, err)
143143
require.Nil(t, key)
144144
}
145+
146+
func TestLoadHardcodedPublicKey_CanBeUsedWithEncryptor(t *testing.T) {
147+
// Test that the hardcoded key can be used to create an encryptor
148+
// First, test that the key can be loaded successfully
149+
key, uid, err := internalrsa.LoadHardcodedPublicKey()
150+
require.NoError(t, err)
151+
require.NotNil(t, key)
152+
require.NotEmpty(t, uid)
153+
154+
encryptor, err := internalrsa.NewEncryptor(uid, key)
155+
require.NoError(t, err)
156+
require.NotNil(t, encryptor)
157+
158+
// Test that the encryptor can encrypt data
159+
testData := []byte("test data for encryption")
160+
encryptedData, err := encryptor.Encrypt(testData)
161+
require.NoError(t, err)
162+
require.NotNil(t, encryptedData)
163+
require.NotEmpty(t, encryptedData.Data)
164+
require.Equal(t, "JWE-RSA", encryptedData.Type)
165+
}

internal/envelope/types.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
package envelope
22

3+
import "encoding/json"
4+
35
// EncryptedData represents encrypted data along with metadata about the encryption type.
46
type EncryptedData struct {
57
// Data contains the encrypted payload
6-
Data []byte
8+
Data []byte `json:"data"`
79
// Type indicates the encryption format (e.g., "JWE-RSA")
8-
Type string
10+
Type string `json:"type"`
11+
}
12+
13+
// ToMap converts the EncryptedData struct to a map representation. Since we store data as an "_encryptedData" field in
14+
// a Kubernetes unstructured object, passing a raw struct would cause a panic due to the behaviour of
15+
// https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#DeepCopyJSONValue
16+
// Passing a map to unstructured.SetNestedField avoids this issue.
17+
func (ed *EncryptedData) ToMap() map[string]interface{} {
18+
marshalled, err := json.Marshal(ed)
19+
if err != nil {
20+
return nil
21+
}
22+
23+
var out map[string]any
24+
25+
err = json.Unmarshal(marshalled, &out)
26+
if err != nil {
27+
return nil
28+
}
29+
30+
return out
931
}
1032

1133
// Encryptor performs envelope encryption on arbitrary data.

0 commit comments

Comments
 (0)