Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ predicate.json

_bin
.envrc

examples/encrypted-secrets/output.json
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ go run . agent \
> - [./agent.yaml](./agent.yaml).
> - [./examples/one-shot-secret.yaml](./examples/one-shot-secret.yaml).
> - [./examples/cert-manager-agent.yaml](./examples/cert-manager-agent.yaml).
> - [./examples/encrypted-secrets](./examples/encrypted-secrets) - Send encrypted Kubernetes secrets to CyberArk.

You might also want to run a local echo server to monitor requests sent by the agent:

Expand Down
7 changes: 7 additions & 0 deletions deploy/charts/disco-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ This cluster name will be associated with the data that the agent uploads to the
A short description of the cluster where the agent is deployed (optional).

This description will be associated with the data that the agent uploads to the Discovery and Context service. The description may include contact information such as the email address of the cluster administrator, so that any problems and risks identified by the Discovery and Context service can be communicated to the people responsible for the affected secrets.
#### **config.sendSecretValues** ~ `bool`
> Default value:
> ```yaml
> false
> ```

Enable sending of Secret values to CyberArk in addition to metadata. Metadata is always sent, but the actual values of Secrets are not sent by default. When enabled, Secret data is encrypted using envelope encryption using a key managed by CyberArk. Default: false (but default will change to true for a future release)
#### **authentication.secretName** ~ `string`
> Default value:
> ```yaml
Expand Down
2 changes: 2 additions & 0 deletions deploy/charts/disco-agent/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ spec:
name: {{ .Values.authentication.secretName }}
key: ARK_DISCOVERY_API
optional: true
- name: ARK_SEND_SECRET_VALUES
value: {{ .Values.config.sendSecretValues | default "false" | quote }}
{{- with .Values.http_proxy }}
- name: HTTP_PROXY
value: {{ . }}
Expand Down
8 changes: 8 additions & 0 deletions deploy/charts/disco-agent/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@
},
"period": {
"$ref": "#/$defs/helm-values.config.period"
},
"sendSecretValues": {
"$ref": "#/$defs/helm-values.config.sendSecretValues"
}
},
"type": "object"
Expand Down Expand Up @@ -148,6 +151,11 @@
"description": "Push data every 12 hours unless changed.",
"type": "string"
},
"helm-values.config.sendSecretValues": {
"default": false,
"description": "Enable sending of Secret values to CyberArk in addition to metadata. Metadata is always sent, but the actual values of Secrets are not sent by default. When enabled, Secret data is encrypted using envelope encryption using a key managed by CyberArk. Default: false (but default will change to true for a future release)",
"type": "boolean"
},
"helm-values.extraArgs": {
"default": [],
"description": "extraArgs:\n- --logging-format=json\n- --log-level=6 # To enable HTTP request logging",
Expand Down
7 changes: 7 additions & 0 deletions deploy/charts/disco-agent/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ config:
# be communicated to the people responsible for the affected secrets.
clusterDescription: ""

# Enable sending of Secret values to CyberArk in addition to metadata.
# Metadata is always sent, but the actual values of Secrets are not sent by default.
# When enabled, Secret data is encrypted using envelope encryption using
# a key managed by CyberArk.
# Default: false (but default will change to true for a future release)
sendSecretValues: false

authentication:
secretName: agent-credentials

Expand Down
47 changes: 47 additions & 0 deletions examples/encrypted-secrets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Encrypted Secrets Example

This example demonstrates how to use the disco agent to gather Kubernetes secrets and encrypt their data fields.

## Overview

When the `ARK_SEND_SECRETS` environment variable is set to `"true"`, the disco agent will:

0. Fetch an encryption key from the configured endpoint (if running in production) or use a local key for testing
1. Discover Kubernetes secrets in your cluster (excluding common system secret types)
2. Encrypt each secret's data fields using RSA envelope encryption with JWE (JSON Web Encryption) format
3. If running in production, send the encrypted secrets to the configured endpoint; otherwise, write them to `output.json` for testing

The encryption uses:

- **Key Algorithm**: RSA-OAEP-256 (for encrypting the content encryption key)
- **Content Encryption**: AES-256-GCM (for encrypting the actual secret data)
- **Format**: JWE Compact Serialization

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.

## Prerequisites

1. A running Kubernetes cluster with secrets to discover
3. Go installed

## Configuration File

The `config.yaml` file configures:

- The data gatherer to collect Kubernetes secrets
- Field selectors to exclude system secrets (service account tokens, docker configs, etc.)
- The cluster ID and organization ID for grouping data

## Running the Example

Test the agent locally by running this script:

```bash
./test.sh
```

This will:

- Connect to your current Kubernetes context
- Gather all non-system secrets
- Write the raw data to `output.json`
41 changes: 41 additions & 0 deletions examples/encrypted-secrets/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# encrypted-secrets config.yaml
#
# An example configuration file demonstrating how to use the disco agent
# to send encrypted secrets to CyberArk Discovery & Context.
#
# The agent will:
# 1. Discover Kubernetes secrets in the cluster
# 2. Encrypt the secret data fields using RSA envelope encryption (JWE format)
# 3. Upload the encrypted secrets to CyberArk Discovery & Context
#
# Example usage:
#
# export ARK_SUBDOMAIN="your-subdomain"
# export ARK_USERNAME="your-username"
# export ARK_SECRET="your-secret"
# export ARK_SEND_SECRETS="true"
#
# go run . agent \
# --agent-config-file examples/encrypted-secrets/config.yaml \
# --one-shot \
# --output-path output.json
#
organization_id: "my-organization"
cluster_id: "my_cluster"
period: 1m
data-gatherers:
- kind: "k8s-dynamic"
name: "k8s/secrets"
config:
resource-type:
version: v1
resource: secrets
# Filter out common system secret types to focus on application secrets
field-selectors:
- type!=kubernetes.io/service-account-token
- type!=kubernetes.io/dockercfg
- type!=kubernetes.io/dockerconfigjson
- type!=kubernetes.io/basic-auth
- type!=kubernetes.io/ssh-auth
- type!=bootstrap.kubernetes.io/token
- type!=helm.sh/release.v1
65 changes: 65 additions & 0 deletions examples/encrypted-secrets/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# test.sh - Test script for the encrypted secrets example
#
# This script demonstrates running the disco agent with encrypted secrets enabled.
# It will run in one-shot mode and output to a local file for inspection.

set -euo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${GREEN}=== Encrypted Secrets Example Test ===${NC}\n"

echo -e "${GREEN}Testing agent with Kubernetes secrets${NC}"
echo ""

# Enable encrypted secrets
export ARK_SEND_SECRETS="true"

# Check Kubernetes connectivity
if ! kubectl cluster-info &> /dev/null; then
echo -e "${RED}Error: Unable to connect to Kubernetes cluster${NC}"
echo "Please ensure your kubeconfig is configured correctly."
exit 1
fi

echo -e "${GREEN}✓ Connected to Kubernetes cluster${NC}"
CONTEXT=$(kubectl config current-context)
echo " Context: ${CONTEXT}"
echo ""

# Check for secrets
SECRET_COUNT=$(kubectl get secrets --all-namespaces --no-headers 2>/dev/null | wc -l | tr -d ' ')
echo "Found ${SECRET_COUNT} secrets in cluster"
echo ""

# Run the agent in one-shot mode with output to file
OUTPUT_FILE="output.json"
echo -e "${GREEN}Running disco agent with encrypted secrets enabled...${NC}"
echo "Command: go run ../.. agent --agent-config-file config.yaml --one-shot --output-path ${OUTPUT_FILE}"
echo ""

if go run ../.. agent \
--agent-config-file config.yaml \
--one-shot \
--output-path "${OUTPUT_FILE}"; then

echo ""
echo -e "${GREEN}✓ Agent completed successfully${NC}"

# Check if output file was created
if [ -f "${OUTPUT_FILE}" ]; then
echo -e "${GREEN}✓ Output file created: ${OUTPUT_FILE}${NC}"
else
echo -e "${RED}✗ Output file was not created${NC}"
exit 1
fi
else
echo ""
echo -e "${RED}✗ Agent failed${NC}"
exit 1
fi
2 changes: 1 addition & 1 deletion internal/cyberark/dataupload/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ func (mds *mockDataUploadServer) handleSnapshotLinks(w http.ResponseWriter, r *h
}

// Write response body
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(struct {
URL string `json:"url"`
}{presignedURL})
Expand Down
32 changes: 32 additions & 0 deletions internal/envelope/rsa/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ import (

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

const (
// HardcodedPublicKeyPEM contains a temporary hardcoded RSA public key (2048-bit) for envelope encryption.
// This is a TEMPORARY solution for initial development and testing.
// TODO: Replace with dynamic key fetching from CyberArk Discovery & Context API.
HardcodedPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoeq+dk4aoGdV9xjrnGJt
VbUh5jvkQgynkP+9Ph2NVeoasXWqYOmOVeKOI7Yr58W/L8Mro6C22iSEJrPFgPF6
t+RJsLAsAY6w1Pocq16COeelAWtxhHQGXt77WQKk0kmwhOJZ4VSeiQC4hWLUnq4N
Ft7lwLw/50opTXLuSErrwec/bEV7G/Xp11BMsHGEL7dzpwWAfIrbCEomyWrO/L6p
O3SAgYMdfup5ddnszeCU2FbFQziOkuMLOyir91XXk8wgdSy4IGAEGpwNx88i8fuj
Qafze2aGWUtpWlOEQPP8lH2cj2TGUgLxGITbczJRcwuGIoJBOzAmPDWi/bapj4b6
zQIDAQAB
-----END PUBLIC KEY-----`

// hardcodedUID is a temporary hardcoded UID associated with the hardcoded public key
// It was randomly generated with the macOS "uuidgen" command
hardcodedUID = "A39798E6-8CE7-4E6E-9CF6-24A3C923B3A7"
)

// LoadPublicKeyFromPEM parses an RSA public key from PEM-encoded bytes.
// The PEM block should be of type "PUBLIC KEY" or "RSA PUBLIC KEY".
func LoadPublicKeyFromPEM(pemBytes []byte) (*rsa.PublicKey, error) {
Expand Down Expand Up @@ -55,3 +74,16 @@ func LoadPublicKeyFromPEMFile(path string) (*rsa.PublicKey, error) {

return LoadPublicKeyFromPEM(pemBytes)
}

// LoadHardcodedPublicKey loads and parses the hardcoded RSA public key.
// Returns a hardcoded UID associated with the key.
// This is a temporary solution for initial development and testing.
// Returns an error if the hardcoded key is invalid or cannot be parsed.
func LoadHardcodedPublicKey() (*rsa.PublicKey, string, error) {
key, err := LoadPublicKeyFromPEM([]byte(HardcodedPublicKeyPEM))
if err != nil {
return nil, "", err
}

return key, hardcodedUID, nil
}
21 changes: 21 additions & 0 deletions internal/envelope/rsa/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,24 @@ func TestLoadPublicKeyFromPEMFile_InvalidContent(t *testing.T) {
require.Error(t, err)
require.Nil(t, key)
}

func TestLoadHardcodedPublicKey_CanBeUsedWithEncryptor(t *testing.T) {
// Test that the hardcoded key can be used to create an encryptor
// First, test that the key can be loaded successfully
key, uid, err := internalrsa.LoadHardcodedPublicKey()
require.NoError(t, err)
require.NotNil(t, key)
require.NotEmpty(t, uid)

encryptor, err := internalrsa.NewEncryptor(uid, key)
require.NoError(t, err)
require.NotNil(t, encryptor)

// Test that the encryptor can encrypt data
testData := []byte("test data for encryption")
encryptedData, err := encryptor.Encrypt(testData)
require.NoError(t, err)
require.NotNil(t, encryptedData)
require.NotEmpty(t, encryptedData.Data)
require.Equal(t, "JWE-RSA", encryptedData.Type)
}
26 changes: 24 additions & 2 deletions internal/envelope/types.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
package envelope

import "encoding/json"

// EncryptedData represents encrypted data along with metadata about the encryption type.
type EncryptedData struct {
// Data contains the encrypted payload
Data []byte
Data []byte `json:"data"`
// Type indicates the encryption format (e.g., "JWE-RSA")
Type string
Type string `json:"type"`
}

// ToMap converts the EncryptedData struct to a map representation. Since we store data as an "_encryptedData" field in
// a Kubernetes unstructured object, passing a raw struct would cause a panic due to the behaviour of
// https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime#DeepCopyJSONValue
// Passing a map to unstructured.SetNestedField avoids this issue.
func (ed *EncryptedData) ToMap() map[string]any {
marshalled, err := json.Marshal(ed)
if err != nil {
return nil
}

var out map[string]any

err = json.Unmarshal(marshalled, &out)
if err != nil {
return nil
}

return out
}

// Encryptor performs envelope encryption on arbitrary data.
Expand Down
31 changes: 31 additions & 0 deletions pkg/agent/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"

"github.com/jetstack/preflight/api"
"github.com/jetstack/preflight/internal/envelope"
"github.com/jetstack/preflight/internal/envelope/rsa"
"github.com/jetstack/preflight/pkg/client"
"github.com/jetstack/preflight/pkg/datagatherer"
"github.com/jetstack/preflight/pkg/datagatherer/k8sdynamic"
Expand Down Expand Up @@ -181,6 +183,19 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
if isDynamicGatherer {
dynDg.ExcludeAnnotKeys = config.ExcludeAnnotationKeysRegex
dynDg.ExcludeLabelKeys = config.ExcludeLabelKeysRegex

// Check if secret encryption is enabled via environment variable
// When enabled, secret data will be kept for encryption instead of being redacted
encryptSecrets := strings.ToLower(os.Getenv("ARK_SEND_SECRET_VALUES"))

if encryptSecrets == "true" {
var err error

dynDg.Encryptor, err = loadEncryptor()
if err != nil {
log.Error(err, "Failed to set up encryptor for secrets, secret data will not be sent")
}
}
}

log.V(logs.Debug).Info("Starting DataGatherer", "name", dgConfig.Name)
Expand Down Expand Up @@ -257,6 +272,22 @@ func Run(cmd *cobra.Command, args []string) (returnErr error) {
return nil
}

// loadEncryptor sets up an encryptor for encrypting secrets. For now, it just loads a hardcoded public key
func loadEncryptor() (envelope.Encryptor, error) {
// TODO(@SgtCoDFish): this will eventually fetch a key from JWKS endpoint when that endpoint is available
key, keyID, err := rsa.LoadHardcodedPublicKey()
if err != nil {
return nil, fmt.Errorf("failed to load public key for secret encryption: %w", err)
}

encryptor, err := rsa.NewEncryptor(keyID, key)
if err != nil {
return nil, fmt.Errorf("failed to create encryptor for secret encryption: %w", err)
}

return encryptor, nil
}

// Creates an event recorder for the agent's Pod object. Expects the env var
// POD_NAME to contain the pod name. Note that the RBAC rule allowing sending
// events is attached to the pod's service account, not the impersonated service
Expand Down
Loading