diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index e1d0cee..7e2366c 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -66,6 +66,8 @@ jobs: echo "SECRET_WITH_FILE=op://${{ secrets.VAULT }}/file-secret/test.txt" >> tests/.env.tpl echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT }}/file-secret/file section/test.txt" >> tests/.env.tpl echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT }}/double-section-secret/test-section/password" >> tests/.env.tpl + echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl + echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl - name: Generate .vaultId_env.tpl shell: bash @@ -76,6 +78,8 @@ jobs: echo "SECRET_WITH_FILE=op://${{ secrets.VAULT_ID }}/file-secret/test.txt" >> tests/.vaultId_env.tpl echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt" >> tests/.vaultId_env.tpl echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password" >> tests/.vaultId_env.tpl + echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key" >> tests/.vaultId_env.tpl + echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.vaultId_env.tpl - name: Configure Service account uses: ./configure @@ -95,6 +99,8 @@ jobs: SECRET_WITH_FILE: op://${{ secrets.VAULT }}/file-secret/test.txt SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT }}/file-secret/file section/test.txt DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT }}/double-section-secret/test-section/password + SSH_PRIVATE_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key + SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh OP_ENV_FILE: ./tests/.env.tpl - name: Assert test secret values [step output] @@ -110,6 +116,8 @@ jobs: SECRET_WITH_FILE: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE }} SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE_IN_SECTION }} DOUBLE_SECTION_SECRET: ${{ steps.load_secrets.outputs.DOUBLE_SECTION_SECRET }} + SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }} + SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }} run: ./tests/assert-env-set.sh - name: Assert test secret values [exported env] @@ -117,6 +125,19 @@ jobs: shell: bash run: ./tests/assert-env-set.sh + - name: Assert SSH keys [step output] + if: ${{ !matrix.export-env }} + shell: bash + env: + SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }} + SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }} + run: ./tests/assert-ssh-keys.sh + + - name: Assert SSH keys [exported env] + if: ${{ matrix.export-env }} + shell: bash + run: ./tests/assert-ssh-keys.sh + - name: Remove secrets [exported env] if: ${{ matrix.export-env }} uses: ./ @@ -156,6 +177,8 @@ jobs: SECRET_WITH_FILE: op://${{ secrets.VAULT_ID }}/file-secret/test.txt SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password + SSH_PRIVATE_KEY: op://${{ secrets.VAULT_ID }}/test-ssh-key/private key + SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh OP_ENV_FILE: ./tests/.vaultId_env.tpl - name: Assert test secret values [vault by ID] @@ -171,6 +194,8 @@ jobs: SECRET_WITH_FILE: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE }} SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets_by_vault_id.outputs.SECRET_WITH_FILE_IN_SECTION }} DOUBLE_SECTION_SECRET: ${{ steps.load_secrets_by_vault_id.outputs.DOUBLE_SECTION_SECRET }} + SSH_PRIVATE_KEY: ${{ steps.load_secrets_by_vault_id.outputs.SSH_PRIVATE_KEY }} + SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets_by_vault_id.outputs.SSH_PRIVATE_KEY_OPENSSH }} run: ./tests/assert-env-set.sh test-connect: @@ -210,6 +235,8 @@ jobs: echo "SECRET_WITH_FILE=op://${{ secrets.VAULT }}/file-secret/test.txt" >> tests/.env.tpl echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT }}/file-secret/file section/test.txt" >> tests/.env.tpl echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT }}/double-section-secret/test-section/password" >> tests/.env.tpl + echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT }}/test-ssh-key/private key" >> tests/.env.tpl + echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.env.tpl - name: Generate .vaultId_env.tpl run: | @@ -219,6 +246,8 @@ jobs: echo "SECRET_WITH_FILE=op://${{ secrets.VAULT_ID }}/file-secret/test.txt" >> tests/.vaultId_env.tpl echo "SECRET_WITH_FILE_IN_SECTION=op://${{ secrets.VAULT_ID }}/file-secret/file section/test.txt" >> tests/.vaultId_env.tpl echo "DOUBLE_SECTION_SECRET=op://${{ secrets.VAULT_ID }}/double-section-secret/test-section/password" >> tests/.vaultId_env.tpl + echo "SSH_PRIVATE_KEY=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key" >> tests/.vaultId_env.tpl + echo "SSH_PRIVATE_KEY_OPENSSH=op://${{ secrets.VAULT_ID }}/test-ssh-key/private key?ssh-format=openssh" >> tests/.vaultId_env.tpl - name: Launch 1Password Connect instance env: @@ -246,6 +275,8 @@ jobs: SECRET_WITH_FILE: op://${{ secrets.VAULT }}/file-secret/test.txt SECRET_WITH_FILE_IN_SECTION: op://${{ secrets.VAULT }}/file-secret/file section/test.txt DOUBLE_SECTION_SECRET: op://${{ secrets.VAULT }}/double-section-secret/test-section/password + SSH_PRIVATE_KEY: op://${{ secrets.VAULT }}/test-ssh-key/private key + SSH_PRIVATE_KEY_OPENSSH: op://${{ secrets.VAULT }}/test-ssh-key/private key?ssh-format=openssh OP_ENV_FILE: ./tests/.env.tpl - name: Assert test secret values [step output] @@ -260,12 +291,27 @@ jobs: SECRET_WITH_FILE: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE }} SECRET_WITH_FILE_IN_SECTION: ${{ steps.load_secrets.outputs.SECRET_WITH_FILE_IN_SECTION }} DOUBLE_SECTION_SECRET: ${{ steps.load_secrets.outputs.DOUBLE_SECTION_SECRET }} + SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }} + SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }} run: ./tests/assert-env-set.sh - name: Assert test secret values [exported env] if: ${{ matrix.export-env }} run: ./tests/assert-env-set.sh + - name: Assert SSH keys [step output] + if: ${{ !matrix.export-env }} + shell: bash + env: + SSH_PRIVATE_KEY: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY }} + SSH_PRIVATE_KEY_OPENSSH: ${{ steps.load_secrets.outputs.SSH_PRIVATE_KEY_OPENSSH }} + run: ./tests/assert-ssh-keys.sh + + - name: Assert SSH keys [exported env] + if: ${{ matrix.export-env }} + shell: bash + run: ./tests/assert-ssh-keys.sh + - name: Remove secrets [exported env] if: ${{ matrix.export-env }} uses: ./ diff --git a/package-lock.json b/package-lock.json index 2900279..0fac887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,15 @@ "@actions/core": "^1.10.1", "@actions/exec": "^1.1.1", "@actions/tool-cache": "^2.0.2", - "dotenv": "^17.2.2" + "dotenv": "^17.2.2", + "sshpk": "^1.18.0" }, "devDependencies": { "@1password/eslint-config": "^4.3.1", "@1password/prettier-config": "^1.2.0", "@types/jest": "^29.5.12", "@types/node": "^20.11.30", + "@types/sshpk": "^1.17.4", "@vercel/ncc": "^0.38.1", "husky": "^9.0.11", "jest": "^29.7.0", @@ -1302,6 +1304,15 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@types/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1426,6 +1437,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sshpk": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/@types/sshpk/-/sshpk-1.17.4.tgz", + "integrity": "sha512-5gI/7eJn6wmkuIuFY8JZJ1g5b30H9K5U5vKrvOuYu+hoZLb2xcVEgxhYZ2Vhbs0w/ACyzyfkJq0hQtBfSCugjw==", + "dev": true, + "dependencies": { + "@types/asn1": "*", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1980,6 +2001,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2180,6 +2217,14 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2637,6 +2682,17 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -2859,6 +2915,15 @@ "node": ">= 0.4" } }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -4092,6 +4157,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5644,6 +5717,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", @@ -7145,6 +7223,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -7406,6 +7489,30 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -7874,6 +7981,11 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 5c9e775..df041c9 100644 --- a/package.json +++ b/package.json @@ -46,13 +46,15 @@ "@actions/core": "^1.10.1", "@actions/exec": "^1.1.1", "@actions/tool-cache": "^2.0.2", - "dotenv": "^17.2.2" + "dotenv": "^17.2.2", + "sshpk": "^1.18.0" }, "devDependencies": { "@1password/eslint-config": "^4.3.1", "@1password/prettier-config": "^1.2.0", "@types/jest": "^29.5.12", "@types/node": "^20.11.30", + "@types/sshpk": "^1.17.4", "@vercel/ncc": "^0.38.1", "husky": "^9.0.11", "jest": "^29.7.0", diff --git a/src/utils.ts b/src/utils.ts index a7c6ab1..3334928 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import * as core from "@actions/core"; +import sshpk from "sshpk"; import { read } from "@1password/op-js"; import { createClient, Secrets } from "@1password/sdk"; import { OnePasswordConnect, FullItem, OPConnect } from "@1password/connect"; @@ -17,6 +18,8 @@ interface ParsedOpRef { item: string; section: string | undefined; field: string; + + queryParams: string | undefined; } export const parseOpRef = (ref: string): ParsedOpRef => { @@ -47,11 +50,14 @@ export const parseOpRef = (ref: string): ParsedOpRef => { throw new Error(`Invalid op reference: item is required`); } - // Last segment is always the field - const field = segments[segments.length - 1] ?? ""; + // Last segment is the field; it may include a query string (e.g. "private key?ssh-format=openssh") + const lastSegment = segments[segments.length - 1] ?? ""; + const [fieldPart, queryPart] = lastSegment.split("?"); + const field = fieldPart ?? ""; if (!field) { throw new Error(`Invalid op reference: field is required`); } + const queryParams = queryPart ?? undefined; // Second to last segment is the section if it exists let section: string | undefined; @@ -69,6 +75,7 @@ export const parseOpRef = (ref: string): ParsedOpRef => { item, field, section, + queryParams, }; }; // #endregion @@ -270,6 +277,19 @@ const createConnectClient = (host: string, token: string): OPConnect => { throw new Error(`Connect authentication failed: ${message}`); } }; + +const toOpenSSH = ( + value: string, + _queryParams: string | undefined, +): string => { + try { + const key = sshpk.parsePrivateKey(value, "auto"); + return key.toString("openssh"); + } catch { + core.warning(`Failed to parse private key to OpenSSH, returning original value`); + return value; + } +}; // #endregion // #region Shared helpers and auth @@ -432,7 +452,11 @@ const loadSecretsViaConnect = async ( // Get the secret value from the item as Connect returns a full item object const secretValue = await getSecretFromConnectItem(client, item, parsed); - setResolvedSecret(envName, secretValue, shouldExportEnv); + let valueToSet = secretValue; + if (parsed.queryParams?.includes("ssh-format=openssh")) { + valueToSet = toOpenSSH(secretValue, parsed.queryParams); + } + setResolvedSecret(envName, valueToSet, shouldExportEnv); } catch (err) { const msg = err instanceof Error ? err.message : String(err); throw new Error(`Failed to load ref "${ref}": ${msg}`); diff --git a/tests/assert-env-unset.sh b/tests/assert-env-unset.sh index e66245c..0b86e03 100755 --- a/tests/assert-env-unset.sh +++ b/tests/assert-env-unset.sh @@ -21,3 +21,6 @@ assert_env_unset "FILE_MULTILINE_SECRET" assert_env_unset "SECRET_WITH_FILE" assert_env_unset "SECRET_WITH_FILE_IN_SECTION" assert_env_unset "DOUBLE_SECTION_SECRET" + +assert_env_unset "SSH_PRIVATE_KEY" +assert_env_unset "SSH_PRIVATE_KEY_OPENSSH" diff --git a/tests/assert-ssh-keys.sh b/tests/assert-ssh-keys.sh new file mode 100755 index 0000000..a98de85 --- /dev/null +++ b/tests/assert-ssh-keys.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# SSH_PRIVATE_KEY: any private key format +v="$(printenv SSH_PRIVATE_KEY)" +if [ -z "$v" ]; then + echo "SSH_PRIVATE_KEY is not set" + exit 1 +fi +if ! echo "$v" | head -1 | grep -qE -- '^-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'; then + echo "SSH_PRIVATE_KEY does not start with a private key header" + exit 1 +fi +if ! echo "$v" | tail -1 | grep -qE -- '-----END (RSA |EC |OPENSSH )?PRIVATE KEY-----$'; then + echo "SSH_PRIVATE_KEY does not end with a private key footer" + exit 1 +fi +echo "SSH_PRIVATE_KEY has valid key format" + +# SSH_PRIVATE_KEY_OPENSSH: OpenSSH format only +v="$(printenv SSH_PRIVATE_KEY_OPENSSH)" +if [ -z "$v" ]; then + echo "SSH_PRIVATE_KEY_OPENSSH is not set" + exit 1 +fi +if ! echo "$v" | head -1 | grep -q -- '-----BEGIN OPENSSH PRIVATE KEY-----'; then + echo "SSH_PRIVATE_KEY_OPENSSH is not in OpenSSH format" + exit 1 +fi +if ! echo "$v" | tail -1 | grep -q -- '-----END OPENSSH PRIVATE KEY-----$'; then + echo "SSH_PRIVATE_KEY_OPENSSH does not end with OpenSSH private key footer" + exit 1 +fi +echo "SSH_PRIVATE_KEY_OPENSSH has valid OpenSSH key format"