diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dccd0fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# Dependencies - we install fresh in Docker +node_modules +npm-debug.log +yarn-error.log + +# Git files - not needed in image +.git +.gitignore +.gitattributes + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store + +# Environment files +.env +.env.local +.env.*.local + +# Build artifacts +dist/ +build/ +*.log + +# Temporary files +tmp/ +temp/ +*.tmp diff --git a/README.md b/README.md index 0c7d841..d757237 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,51 @@ -# Harness Keeper Security Plugin +# Harness CI Keeper Plugin -A Harness CI/CD plugin that integrates with Keeper Security Secrets Manager (KSM) to securely fetch and inject secrets into your CI/CD pipelines at runtime. +Keeper Secrets Manager integration into Harness CI for dynamic secrets retrieval ## Overview This plugin securely retrieves secrets from Keeper Security Secrets Manager and makes them available to subsequent steps in your Harness pipeline. The plugin implements a zero-knowledge architecture where secrets are retrieved directly from Keeper Vault at runtime and never pass through Harness systems in decrypted form. -**Key Features:** -- **Zero-Knowledge Architecture**: Secrets are retrieved directly from Keeper Vault at runtime -- **Multiple Authentication Methods**: Supports one-time access tokens (`US:...`) and full JSON config -- **Keeper Notation Support**: Query fields (`field`), custom fields (`custom_field`), and file attachments (`file`) -- **Multiple Output Formats**: Environment variables, file output, and output variables -- **Secure Storage**: Secrets written to `/harness/secrets/` with secure file permissions -- **Automatic Validation**: Validates token format and required config fields -- **Base64 Decoding**: Automatically decodes base64-encoded tokens +## Features -## Architecture - -### Zero-Knowledge Design - -The plugin ensures zero-knowledge security through the following design: - -1. **Harness CI/CD** calls the custom Docker-based plugin -2. **Plugin authenticates** using KSM configuration stored in Harness Secrets Manager -3. **Secrets are retrieved** directly from Keeper Vault at runtime -4. **Decrypted secrets** are returned as step outputs for downstream stages -5. **No external storage**: Secrets never pass through Harness systems in decrypted form +- Retrieve secrets from the Keeper Vault within the Harness CI pipeline +- Support for standard fields, custom fields, and file attachments +- Zero-knowledge architecture - secrets retrieved directly from Keeper at runtime All secret flow happens strictly between the plugin container and Keeper, while Harness provides orchestration, governance, and secure secret injection across the pipeline. -### Components - -1. **`src/index.js`**: Main plugin logic - - Token processing and validation - - Keeper Secrets Manager integration - - Secret fetching using Keeper Notation - - Secret distribution - -2. **`entrypoint.sh`**: Container entrypoint script - - Executes the Node.js plugin - - Captures and processes plugin output - - Writes secrets to Harness directories - - Manages security and cleanup - -3. **`Example/Pipeline.yaml`**: Example Harness CI pipeline - - Demonstrates plugin configuration - - Shows secret consumption patterns - -### Execution Flow - -``` -1. Container starts → entrypoint.sh executes -2. entrypoint.sh → Runs: node /app/src/index.js -3. Plugin processes: - - Reads KSM_CONFIG from environment - - Validates and decodes token (base64 if needed) - - Initializes Keeper storage - - Fetches secrets from Keeper using Keeper Notation - - Outputs secrets to stdout (with ENV:/OUT: prefixes) -4. entrypoint.sh captures stdout: - - Parses ENV:/OUT: prefixes - - Writes to /harness/outputs/outputs.txt (output variables) - - Writes to /harness/secrets/ (file-based access) - - Sets secure permissions (chmod 600) -5. Subsequent steps access secrets via: - - Files: /harness/secrets/VARIABLE_NAME (recommended) -``` - ## Prerequisites -- **Keeper Secrets Manager access** ([Quick Start Guide](https://docs.keeper.io/secrets-manager/secrets-manager/quick-start-guide)) -- **Secrets Manager addon enabled** for your Keeper account -- **Membership in a Role** with the Secrets Manager enforcement policy enabled -- **A Keeper Secrets Manager Application** with secrets shared to it -- **An initialized Keeper Secrets Manager Configuration** (JSON or one-time token) -- **Record UID(s)** for the secrets you want to access ⚠️ **REQUIRED** -- **Harness CI/CD account** with a project set up -- **Docker** (for building the plugin image) -- **Node.js 18+** (for local development) +- Keeper Secrets Manager access with Application configured +- Harness CI account with project setup +- KSM configuration (one-time access token `US:...` or Base64-encoded token or JSON confign) -## Installation +## About -### Build and Push Docker Image +The plugin retrieves secrets from Keeper Secrets Manager and stores them in **Harness pipeline volume storage** at `/harness/secrets/` directory for reliable file-based access by subsequent steps. -Build the plugin Docker image: +**Important:** Secrets are stored in `/harness/secrets/` directory (Harness shared workspace volume), not as Harness environment variables. -```bash -docker build -t your-dockerhub-username/harness-keeper-plugin:latest . +All secrets are: +- **Pipeline-scoped**: Only accessible during the current pipeline execution +- **Automatically cleaned up**: Harness CI removes all secrets after pipeline completion +- **Securely stored**: Files have restricted permissions (600 - owner read/write only) + +### Project Structure + +``` +harness_keeper_plugin/ +├── src/ +│ └── index.js # Main plugin logic +├── entrypoint.sh # Container entrypoint script +├── Dockerfile # Docker image definition +├── package.json # Node.js dependencies +├── Example/ +│ └── Pipeline.yaml # Example Harness CI pipeline +└── README.md # This file ``` -Push to your container registry: - -```bash -docker push your-dockerhub-username/harness-keeper-plugin:latest -``` - -**Note:** Replace `your-dockerhub-username` with your actual Docker Hub username or container registry path. - -## Configuration - -### 1. Create Keeper Access Token - -#### Option A: One-Time Access Token -1. Log in to Keeper Security -2. Navigate to Secrets Manager -3. Generate a one-time access token (format: `US:xxxxx`) -4. Base64 encode (optional but recommended): - ```bash - echo -n "US:your-token-here" | base64 - ``` - -#### Option B: Full JSON Config -Create a JSON config with: -```json -{ - "hostname": "keepersecurity.com", - "clientId": "your-client-id", - "privateKey": "your-private-key" -} -``` -Base64 encode: -```bash -echo -n '{"hostname":"...","clientId":"...","privateKey":"..."}' | base64 -``` - -### 2. Create Harness Secret - -In Harness, create a secret: -- **Type**: TEXT or FILE (both work) -- **Name**: `KSM_CONFIG` (or your preferred name) -- **Value**: Your base64-encoded token or JSON config - -### 3. Configure Pipeline - -**Before you begin:** You must have the Record UID(s) for the secrets you want to access. See "How to Get Record UID" section below. +## Quick Start ```yaml pipeline: @@ -161,15 +73,15 @@ pipeline: name: Fetch_Keeper_Secrets identifier: Fetch_Keeper_Secrets spec: - image: your-dockerhub-username/harness-keeper-plugin:latest + image: dhborse/keeper-harness-plugin settings: secrets: | - JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD - JQPso5SMBpP29gKKGp6Enw/field/login > env:DB_USERNAME - JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > env:API_KEY - JQPso5SMBpP29gKKGp6Enw/file/api.key > file:/app/config/api.key + VeYTRo-PHElAwfQT6f0TIA/field/password > DB_PASSWORD + VeYTRo-PHElAwfQT6f0TIA/field/login > DB_USERNAME + VeYTRo-PHElAwfQT6f0TIA/custom_field/text > KEEPER_TEXT + VeYTRo-PHElAwfQT6f0TIA/file/credentials.txt > FILE_DATA envVariables: - KSM_CONFIG: <+secrets.getValue("KSM_CONFIG")> + KSM_CONFIG: <+secrets.getValue("Test_File_secret")> - step: type: Run name: Use_Keeper_Secrets @@ -178,116 +90,79 @@ pipeline: image: alpine:3.20 shell: Sh command: | - DB_USERNAME=$(cat /harness/secrets/DB_USERNAME) - DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD) - API_KEY=$(cat /harness/secrets/API_KEY) - echo "Secrets retrieved successfully" - # Use secrets in your build/deploy process + if [ -f /harness/secrets/DB_USERNAME ] && [ -f /harness/secrets/DB_PASSWORD ]; then + DB_USERNAME=$(cat /harness/secrets/DB_USERNAME) + DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD) + echo "Username: $DB_USERNAME" + echo "Password: $DB_PASSWORD" + fi + + if [ -f /harness/secrets/FILE_DATA ]; then + FILE_DATA=$(cat /harness/secrets/FILE_DATA) + echo "File Data: $FILE_DATA" + fi ``` -**Note:** `envVariables` is used ONLY to pass the Keeper configuration token to the plugin. The actual secrets retrieved from Keeper are stored in Harness pipeline volume storage at `/harness/secrets/`. +## Inputs -## Usage +### KSM_CONFIG -### Secret Mapping Format +Keeper Secrets Manager configuration for authentication. Store in Harness secrets and reference: -``` - > +```yaml +envVariables: + KSM_CONFIG: <+secrets.getValue("Keeper_Config_Secret")> ``` -### Keeper Notation Format +**Supported Formats:** +- One-time access token: `US:xxxxx` +- JSON configuration: `{"hostname": "...", "clientId": "...", "privateKey": "..."}` +- Base64-encoded token or JSON config + +### Secrets + +Keeper Notation queries mapping secrets to destinations: + +**Format:** ` > ` + +**Example:** +```yaml +secrets: | + VeYTRo-PHElAwfQT6f0TIA/field/password > DB_PASSWORD + VeYTRo-PHElAwfQT6f0TIA/field/login > DB_USERNAME + VeYTRo-PHElAwfQT6f0TIA/custom_field/text > KEEPER_TEXT + VeYTRo-PHElAwfQT6f0TIA/file/credentials.txt > FILE_DATA +``` + +## Keeper Notation Format The plugin supports three types of Keeper Notation queries: -1. **Standard Fields**: `/field/` - - Examples: `password`, `login`, `url`, `text`, `host`, etc. +| Type | Format | Example | +|------|--------|---------| +| **Standard Fields** | `/field/` | `VeYTRo.../field/password` | +| **Custom Fields** | `/custom_field/` | `VeYTRo.../custom_field/API_Key` | +| **File Attachments** | `/file/` | `VeYTRo.../file/credentials.txt` | -2. **Custom Fields**: `/custom_field/` - - Examples: `API_Key`, `My Custom Field`, `Environment`, etc. +**Note:** Record UID is the unique identifier for a secret record in Keeper. Get it from Keeper Vault → Record details → Record UID. -3. **File Attachments**: `/file/` - - Examples: `api.key`, `certificate.pem`, `config.json`, etc. +## Destination Format -**Important:** The first part (e.g., `JQPso5SMBpP29gKKGp6Enw`) is the **Record UID**, the unique identifier for a specific secret record in Keeper. +The destination defines where the secret is stored: -### Destination Types +| Format | Description | Output Location | +|--------|-------------|----------------| +| `VARIABLE_NAME` | Default output (recommended) | `/harness/secrets/VARIABLE_NAME` | -| Destination Prefix | Description | Output Location | -|-------------------|-------------|----------------| -| `env:VARIABLE_NAME` | Environment variable | `/harness/secrets/VARIABLE_NAME` | -| `file:/path/to/file` | File output | Specified file path | -| `VARIABLE_NAME` (default) | Output variable | `/harness/secrets/VARIABLE_NAME` | - -### Examples - -#### 1. Standard Field → Environment Variable +**Examples:** ```yaml -secrets: | - JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD - JQPso5SMBpP29gKKGp6Enw/field/login > env:DB_USERNAME -``` -- Creates files: `/harness/secrets/DB_PASSWORD`, `/harness/secrets/DB_USERNAME` -- Access: `DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)` - -#### 2. Custom Field → Output Variable -```yaml -secrets: | - JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > API_KEY - JQPso5SMBpP29gKKGp6Enw/custom_field/Environment > ENV_NAME -``` -- Creates files: `/harness/secrets/API_KEY`, `/harness/secrets/ENV_NAME` -- Access: `API_KEY=$(cat /harness/secrets/API_KEY)` - -#### 3. File Attachment → File Path -```yaml -secrets: | - JQPso5SMBpP29gKKGp6Enw/file/api.key > file:/app/config/api.key - JQPso5SMBpP29gKKGp6Enw/file/certificate.pem > file:/app/ssl/cert.pem -``` -- Downloads files and writes to specified paths -- Access: `cat /app/config/api.key` - -#### 4. Mixed Usage -```yaml -secrets: | - JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD - JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > API_KEY - JQPso5SMBpP29gKKGp6Enw/file/config.json > file:/app/config.json +# Default: saves to /harness/secrets/DB_PASSWORD +VeYTRo-PHElAwfQT6f0TIA/field/password > DB_PASSWORD ``` -### How to Get Record UID +## Accessing Secrets -The Record UID is **mandatory** and identifies which specific secret record to retrieve. Obtain it using one of these methods: - -**Method 1: From Keeper Web Vault** -1. Log in to your Keeper Vault -2. Open the secret record you want to access -3. Click on the record details/info icon -4. The Record UID is displayed in the record information (usually at the bottom) -5. Copy the Record UID (format: `JQPso5SMBpP29gKKGp6Enw`) - -**Method 2: Using Keeper Commander CLI** -```bash -keeper list -# This will show all records with their UIDs -``` - -**Method 3: Using Keeper Secrets Manager SDK/API** -- Use the Keeper Secrets Manager SDK to list records and retrieve their UIDs -- The Record UID is returned when querying records - -**Method 4: From Keeper Admin Console** -1. Navigate to Secrets Manager in Admin Console -2. View the Application and its shared records -3. Record UIDs are visible in the record list - -**Note:** The one-time access token (`US:...`) or JSON config is used to **authenticate** and access Keeper, but you still need the **Record UID** to specify which secret record to retrieve. - -## Accessing Secrets in Subsequent Steps - -### Method 1: File-Based Access (Recommended) - -The most reliable method for accessing secrets in subsequent steps: +Secrets are stored in `/harness/secrets/` directory. Read them in subsequent steps: ```yaml - step: @@ -297,177 +172,52 @@ The most reliable method for accessing secrets in subsequent steps: image: alpine:3.20 shell: Sh command: | - DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD) DB_USERNAME=$(cat /harness/secrets/DB_USERNAME) - API_KEY=$(cat /harness/secrets/API_KEY) - # Use secrets in your process - echo "Secrets loaded successfully" - # Clean up after use (optional) - rm -f /harness/secrets/DB_PASSWORD /harness/secrets/DB_USERNAME + DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD) + FILE_DATA=$(cat /harness/secrets/FILE_DATA) + # Use secrets in your build/deploy process ``` **Benefits:** - Most reliable method - No truncation issues -- Works with all secret types +- Works with all secret types including binary files - Supports long values and special characters -### Method 2: Direct File Access (for file: prefix) +## Secret Storage -When using `file:` prefix, files are written directly to the specified path: - -```yaml -- step: - type: Run - name: Use_File - spec: - command: | - # File already at /app/config/api.key - cat /app/config/api.key -``` - -## Secret Storage Mechanism - -**Important:** This plugin uses **Harness pipeline volume storage**, not Harness environment variables. - -- **Storage Location:** `/harness/secrets/` directory (Harness shared workspace volume) -- **Storage Type:** File-based storage on pipeline-scoped volume -- **Access Method:** Read files directly using `cat` or file operations +- **Location:** `/harness/secrets/` directory (Harness shared workspace volume) +- **Permissions:** `600` (owner read/write only) - **Scope:** Pipeline execution only - automatically cleaned up after completion -- **Permissions:** Files have `600` permissions (owner read/write only) +- **Access:** Read files directly using `cat` or file operations -The `envVariables` section in the plugin step is **only** used to pass the `KSM_CONFIG` (Keeper configuration token) to the plugin. The actual secrets retrieved from Keeper are written to files in `/harness/secrets/` directory. +The `envVariables` section is **only** used to pass `KSM_CONFIG` to the plugin. Actual secrets are written to files in `/harness/secrets/` directory. ## Security -### Zero-Knowledge Architecture - -- **Direct Retrieval**: Secrets are retrieved directly from Keeper Vault at runtime +- **Zero-Knowledge**: Secrets retrieved directly from Keeper Vault at runtime - **No Decrypted Storage**: Secrets never pass through Harness systems in decrypted form -- **Pipeline-Scoped**: All secrets are scoped to the current pipeline execution -- **Automatic Cleanup**: Harness CI automatically cleans up `/harness/outputs/` and `/harness/secrets/` after pipeline completion -- **File Permissions**: Secrets written to `/harness/secrets/` have `600` permissions (owner read/write only) -- **No Persistence**: Secrets are not accessible outside the pipeline execution context +- **Pipeline-Scoped**: All secrets scoped to current pipeline execution +- **Automatic Cleanup**: Harness CI removes secrets after pipeline completion +- **File Permissions**: Files have `600` permissions (owner read/write only) ### Best Practices -1. **Use one-time access tokens** instead of permanent credentials when possible -2. **Base64 encode tokens** before storing in Harness secrets -3. **Clean up secret files** after use in pipeline steps -4. **Use appropriate secret scope** (project/org/account) -5. **Verify Record UIDs** before configuring pipelines +1. Use one-time access tokens instead of permanent credentials +2. Base64 encode tokens before storing in Harness secrets +3. Clean up secret files after use in pipeline steps +4. Use appropriate secret scope (project/org/account) +5. Verify Record UIDs before configuring pipelines ## Troubleshooting -### Token Not Found - -**Error:** `KSM config is required` - -**Solutions:** -- Verify the secret exists in Harness -- Check the secret scope matches your project -- Ensure the expression `<+secrets.getValue("KSM_CONFIG")>` is correct -- Verify secret name matches exactly - -### Invalid Token Format - -**Error:** `Invalid token format - expected US:xxxxx or JSON config` - -**Solutions:** -- Verify the token starts with `US:` (for one-time tokens) -- Check if the token is base64 encoded (it will be auto-decoded) -- For JSON config, ensure it starts with `{` and has required fields: `hostname`, `clientId`, `privateKey` - -### Token Already Used / Expired - -**Error:** `Failed: token ... invalid/expired` - -**Solutions:** -1. Generate a new token in Keeper Security -2. Base64 encode: `echo -n "US:new-token" | base64` -3. Update the Harness secret with the new token -4. Run the pipeline again - -**Note:** One-time access tokens are single-use - generate a new token for each use or use JSON config for reusable credentials. - -### Secret File Not Found - -**Error:** `ERROR: Secret files not found` or `Value not found for notation: /field/` - -**Solutions:** -- **Verify Record UID is correct**: The Record UID must match exactly (case-sensitive) -- **Check Record UID exists**: Ensure the record exists in Keeper and is shared to your Secrets Manager Application -- **Verify access permissions**: The one-time token or JSON config must have access to the record -- **Check field name**: Ensure the field type (`password`, `login`, `text`, etc.) or custom field label exists in the record -- **Verify Application sharing**: The record must be shared to the Secrets Manager Application you're using -- **Check logs**: `ls -la /harness/secrets/` to see what files were created -- **Verify notation format**: Ensure the notation follows the correct format (e.g., `/field/password`, `/custom_field/MyLabel`, `/file/filename`) - -### Expression Not Resolved - -**Error:** Token preview shows `${...}` or `<+...>` unresolved - -**Solutions:** -- Use `envVariables` (not `settings`) for Harness expressions when passing `KSM_CONFIG` to the plugin -- Verify secret name matches exactly -- Check secret scope matches the reference -- Ensure you're using the correct Harness expression syntax: `<+secrets.getValue("SECRET_NAME")>` - -### Harness Expression Not Resolved - -**Error:** `Harness expression not resolved` - -**Solutions:** -- Ensure `KSM_CONFIG` is passed via `envVariables`, not `settings` -- Verify the secret exists and is accessible in the current scope -- Check that the Harness expression syntax is correct: `<+secrets.getValue("SECRET_NAME")>` - -## Local Development - -### Test Locally - -```bash -# Set environment variables -export PLUGIN_SECRETS="JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD" -export KSM_CONFIG="VVM6..." # Your base64 encoded token or JSON config - -# Run the plugin -node ./src/index.js -``` - -### Project Structure - -``` -harness_keeper_plugin/ -├── src/ -│ └── index.js # Main plugin logic -├── entrypoint.sh # Container entrypoint script -├── Dockerfile # Docker image definition -├── package.json # Node.js dependencies -├── Example/ -│ └── Pipeline.yaml # Example Harness CI pipeline -└── README.md # This file -``` - -## Example Pipeline - -See `Example/Pipeline.yaml` for a complete working example that demonstrates: -- Plugin configuration -- Secret mapping with multiple record UIDs -- File-based secret consumption -- Error handling and failure strategies - -## Support - -For issues or questions: -- Check the [Keeper Security Documentation](https://docs.keeper.io/secrets-manager/) -- Review [Keeper Notation Documentation](https://docs.keeper.io/secrets-manager/secrets-manager/keeper-notation/) -- Review Harness CI documentation for plugin configuration -- Verify token format and Keeper record permissions - -## License - -ISC +| Error | Solution | +|-------|----------| +| `KSM config is required` | Verify secret exists and expression `<+secrets.getValue("SECRET_NAME")>` is correct | +| `Invalid token format` | Check token starts with `US:` or `{` for JSON config | +| `Value not found for notation` | Verify Record UID, field name (case-sensitive), and Application permissions | +| `Failed to download file` | Check file exists in record, name matches exactly, and Application has file access permissions | +| `Harness expression not resolved` | Verify secret reference expression and secret scope (project/org/account) | ## Version History @@ -478,3 +228,8 @@ ISC - File-based secret access - Base64 token decoding - Secure file permissions and cleanup + +## References + +- [Keeper Notation Documentation](https://docs.keeper.io/en/keeperpam/secrets-manager/about/keeper-notation) +- [Keeper Secrets Manager Quick Start Guide](https://docs.keeper.io/en/keeperpam/secrets-manager/quick-start-guide) \ No newline at end of file diff --git a/src/index.js b/src/index.js index 7c8ba7b..4284310 100644 --- a/src/index.js +++ b/src/index.js @@ -120,19 +120,42 @@ const runPlugin = async () => { continue; } + const notationIsFile = input.notation.includes('/file/'); + const isFileReference = typeof secret === 'object' && + secret !== null && + (secret.fileId || secret.fileUid || secret.url || notationIsFile); + + let data; + + if (isFileReference) { + try { + const fileData = await downloadFile(secret); + data = fileData instanceof Uint8Array ? Buffer.from(fileData) : + Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData); + } catch (downloadError) { + core.error(`Failed to download file for notation ${input.notation}: ${downloadError.message}`); + continue; + } + } else { + data = Buffer.isBuffer(secret) ? secret : + typeof secret === 'string' ? Buffer.from(secret, 'utf8') : + Buffer.from(String(secret), 'utf8'); + } + if (input.destinationType === 'file') { const fullPath = path.resolve(input.destination); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - const data = (typeof secret === 'object' && secret.fileId) - ? await downloadFile(secret) - : secret; fs.writeFileSync(fullPath, data); - } else if (input.destinationType === 'environment') { - // Output with ENV: prefix for environment variables - console.log(`ENV:${input.destination}='${secret}'`); } else { - // Output variable (for Harness output variables) - console.log(`OUT:${input.destination}='${secret}'`); + fs.mkdirSync('/harness/secrets', { recursive: true }); + const secretFilePath = path.join('/harness/secrets', input.destination); + fs.writeFileSync(secretFilePath, data); + fs.chmodSync(secretFilePath, 0o600); + + if (input.destinationType === 'environment') { + const outputValue = Buffer.isBuffer(data) ? data.toString('utf8') : String(data); + console.log(`ENV:${input.destination}='${outputValue}'`); + } } } } catch (error) { @@ -144,4 +167,4 @@ const runPlugin = async () => { } }; -runPlugin(); +runPlugin(); \ No newline at end of file