mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 04:26:11 +00:00
security + performance improvements to ssh fns
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import { execSync } from "child_process";
|
import { execFile } from "child_process";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import fs from "fs";
|
import { promises as fs } from "fs";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -13,6 +16,8 @@ import {
|
|||||||
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
export const createSshCertSerialNumber = () => {
|
export const createSshCertSerialNumber = () => {
|
||||||
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||||
@@ -23,17 +28,17 @@ export const createSshCertSerialNumber = () => {
|
|||||||
/**
|
/**
|
||||||
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
||||||
* We use this function because the key format generated by `ssh-keygen` is unique.
|
* We use this function because the key format generated by `ssh-keygen` is unique.
|
||||||
|
* @param keyAlgorithm - The key algorithm to use for generating the SSH key pair
|
||||||
|
* @param comment - The comment to use for the SSH key pair
|
||||||
|
* @returns The public and private keys for the SSH key pair
|
||||||
*/
|
*/
|
||||||
export const createSshKeyPair = (keyAlgorithm: CertKeyAlgorithm, comment: string) => {
|
export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm, comment: string) => {
|
||||||
const uniqueId = crypto.randomBytes(8).toString("hex"); // to avoid collions if high-volume key generation
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||||
const privateKeyFile = `ssh_key_${uniqueId}`; // temp key path
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
const publicKeyFile = `${privateKeyFile}.pub`;
|
const publicKeyFile = `${privateKeyFile}.pub`;
|
||||||
|
|
||||||
if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile);
|
let keyType: string;
|
||||||
if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile);
|
let keyBits: string;
|
||||||
|
|
||||||
let keyType = "";
|
|
||||||
let keyBits = "";
|
|
||||||
|
|
||||||
switch (keyAlgorithm) {
|
switch (keyAlgorithm) {
|
||||||
case CertKeyAlgorithm.RSA_2048:
|
case CertKeyAlgorithm.RSA_2048:
|
||||||
@@ -58,41 +63,40 @@ export const createSshKeyPair = (keyAlgorithm: CertKeyAlgorithm, comment: string
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
execSync(`ssh-keygen -t ${keyType} -b ${keyBits} -f ${privateKeyFile} -N '' -C "${comment}"`);
|
try {
|
||||||
|
// Generate the SSH key pair
|
||||||
|
// The "-N ''" sets an empty passphrase
|
||||||
|
// The keys are created in the temporary directory
|
||||||
|
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", "", "-C", comment]);
|
||||||
|
|
||||||
const publicKey = fs.readFileSync(publicKeyFile, "utf8");
|
// Read the generated keys
|
||||||
const privateKey = fs.readFileSync(privateKeyFile, "utf8");
|
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
||||||
|
const privateKey = await fs.readFile(privateKeyFile, "utf8");
|
||||||
|
|
||||||
fs.unlinkSync(privateKeyFile);
|
return { publicKey, privateKey };
|
||||||
fs.unlinkSync(publicKeyFile);
|
} finally {
|
||||||
|
// Cleanup the temporary directory and all its contents
|
||||||
return { publicKey, privateKey };
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the SSH public key for the given SSH private key.
|
* Return the SSH public key for the given SSH private key.
|
||||||
* @param privateKey - The SSH private key to get the public key for
|
* @param privateKey - The SSH private key to get the public key for
|
||||||
*/
|
*/
|
||||||
export const getSshPublicKey = (privateKey: string) => {
|
export const getSshPublicKey = async (privateKey: string) => {
|
||||||
const uniqueId = crypto.randomBytes(8).toString("hex");
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||||
const privateKeyFile = `ssh_key_${uniqueId}`;
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
const publicKeyFile = `${privateKeyFile}.pub`;
|
try {
|
||||||
|
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
||||||
|
|
||||||
if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile);
|
// Run ssh-keygen to extract the public key
|
||||||
if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile);
|
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
|
||||||
|
return stdout.trim();
|
||||||
fs.writeFileSync(privateKeyFile, privateKey);
|
} finally {
|
||||||
fs.chmodSync(privateKeyFile, 0o600);
|
// Ensure that files and the temporary directory are cleaned up
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
const command = `ssh-keygen -y -f ${privateKeyFile} > ${publicKeyFile}`;
|
}
|
||||||
execSync(command);
|
|
||||||
|
|
||||||
const publicKey = fs.readFileSync(publicKeyFile, "utf8");
|
|
||||||
|
|
||||||
fs.unlinkSync(privateKeyFile);
|
|
||||||
fs.unlinkSync(publicKeyFile);
|
|
||||||
|
|
||||||
return publicKey;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,47 +224,53 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
|||||||
/**
|
/**
|
||||||
* Create an SSH certificate for a user or host.
|
* Create an SSH certificate for a user or host.
|
||||||
*/
|
*/
|
||||||
export const createSshCert = ({ caPrivateKey, userPublicKey, keyId, principals, ttl, certType }: TCreateSshCertDTO) => {
|
export const createSshCert = async ({
|
||||||
const uniqueId = crypto.randomBytes(8).toString("hex");
|
caPrivateKey,
|
||||||
const publicKeyFile = `user_key_${uniqueId}.pub`;
|
userPublicKey,
|
||||||
const privateKeyFile = `ssh_ca_key_${uniqueId}`;
|
keyId,
|
||||||
const signedPublicKeyFile = `user_key_${uniqueId}-cert.pub`;
|
principals,
|
||||||
|
ttl,
|
||||||
|
certType
|
||||||
|
}: TCreateSshCertDTO) => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-cert-"));
|
||||||
|
|
||||||
if (fs.existsSync(publicKeyFile)) fs.unlinkSync(publicKeyFile);
|
const publicKeyFile = path.join(tempDir, "user_key.pub");
|
||||||
if (fs.existsSync(privateKeyFile)) fs.unlinkSync(privateKeyFile);
|
const privateKeyFile = path.join(tempDir, "ca_key");
|
||||||
if (fs.existsSync(signedPublicKeyFile)) fs.unlinkSync(signedPublicKeyFile);
|
const signedPublicKeyFile = path.join(tempDir, "user_key-cert.pub");
|
||||||
|
|
||||||
// write public and private keys to temp files
|
|
||||||
fs.writeFileSync(publicKeyFile, userPublicKey);
|
|
||||||
fs.writeFileSync(privateKeyFile, caPrivateKey);
|
|
||||||
fs.chmodSync(privateKeyFile, 0o600);
|
|
||||||
|
|
||||||
const serialNumber = createSshCertSerialNumber();
|
const serialNumber = createSshCertSerialNumber();
|
||||||
|
|
||||||
const certOptions = [
|
// Build `ssh-keygen` arguments for signing
|
||||||
`-s ${privateKeyFile}`, // path to SSH CA private key
|
// Using an array avoids shell injection issues
|
||||||
`-I "${keyId}"`, // identity for the issued certificate (key id)
|
const sshKeygenArgs = [
|
||||||
`-n "${principals.join(",")}"`, // principal(s) that is user(s) or host(s)
|
certType === "host" ? "-h" : null, // host certificate if needed
|
||||||
`-V +${ttl}s`, // TTL in seconds (validity period) for the issue certificate
|
"-s",
|
||||||
`-z ${serialNumber}`, // custom serial number for certificate
|
privateKeyFile, // path to SSH CA private key
|
||||||
certType === "host" ? "-h" : "", // host certificate flag
|
"-I",
|
||||||
publicKeyFile // path to signed [publicKey]
|
keyId, // identity (key ID)
|
||||||
]
|
"-n",
|
||||||
.filter(Boolean)
|
principals.join(","), // principals
|
||||||
.join(" ");
|
"-V",
|
||||||
|
`+${ttl}s`, // validity (TTL in seconds)
|
||||||
|
"-z",
|
||||||
|
serialNumber, // serial number
|
||||||
|
publicKeyFile // public key file to sign
|
||||||
|
].filter(Boolean) as string[];
|
||||||
|
|
||||||
const command = `ssh-keygen ${certOptions}`;
|
try {
|
||||||
|
// Write public and private keys to the temp directory
|
||||||
|
await fs.writeFile(publicKeyFile, userPublicKey, { mode: 0o600 });
|
||||||
|
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
||||||
|
|
||||||
console.log("executing command", command);
|
// Execute the signing process
|
||||||
|
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
|
||||||
|
|
||||||
// Execute the signing process
|
// Read the signed public key from the generated cert file
|
||||||
execSync(command);
|
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
||||||
|
|
||||||
const signedPublicKey = fs.readFileSync(signedPublicKeyFile, "utf8");
|
return { serialNumber, signedPublicKey };
|
||||||
|
} finally {
|
||||||
fs.unlinkSync(publicKeyFile);
|
// Cleanup the temporary directory and all its contents
|
||||||
fs.unlinkSync(privateKeyFile);
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
fs.unlinkSync(signedPublicKeyFile);
|
}
|
||||||
|
|
||||||
return { serialNumber, signedPublicKey };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
const { publicKey, privateKey } = createSshKeyPair(keyAlgorithm, ca.friendlyName);
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm, ca.friendlyName);
|
||||||
|
|
||||||
// TODO: update to sshEncryptor
|
// TODO: update to sshEncryptor
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
@@ -151,7 +151,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const publicKey = getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
return { ...ca, publicKey };
|
return { ...ca, publicKey };
|
||||||
};
|
};
|
||||||
@@ -175,7 +175,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const publicKey = getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
return publicKey;
|
return publicKey;
|
||||||
};
|
};
|
||||||
@@ -223,7 +223,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const publicKey = getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
|
||||||
|
|
||||||
return { ...updatedCa, publicKey };
|
return { ...updatedCa, publicKey };
|
||||||
};
|
};
|
||||||
@@ -329,9 +329,9 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// create user key pair
|
// create user key pair
|
||||||
const { publicKey, privateKey } = createSshKeyPair(keyAlgorithm, "Client Key");
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm, "Client Key");
|
||||||
|
|
||||||
const { serialNumber, signedPublicKey } = createSshCert({
|
const { serialNumber, signedPublicKey } = await createSshCert({
|
||||||
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
userPublicKey: publicKey,
|
userPublicKey: publicKey,
|
||||||
keyId,
|
keyId,
|
||||||
@@ -460,7 +460,7 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const { serialNumber, signedPublicKey } = createSshCert({
|
const { serialNumber, signedPublicKey } = await createSshCert({
|
||||||
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
userPublicKey: publicKey,
|
userPublicKey: publicKey,
|
||||||
keyId,
|
keyId,
|
||||||
|
|||||||
Reference in New Issue
Block a user