feat: implement OpenBao PKI certificate authentication
This commit is contained in:
@@ -23,7 +23,7 @@ function counter(onBytes) {
|
|||||||
// Connect the upstream ssh2.Client, retrying once after a short pause if the
|
// Connect the upstream ssh2.Client, retrying once after a short pause if the
|
||||||
// first attempt fails auth (SSSD/AuthorizedKeysCommand cache lag right after a
|
// first attempt fails auth (SSSD/AuthorizedKeysCommand cache lag right after a
|
||||||
// first-time key injection).
|
// first-time key injection).
|
||||||
function connectUpstream({ host, port, username, privateKey, onHostKey, uid, justInjected }) {
|
function connectUpstream({ host, port, username, privateKey, cert, onHostKey, uid, justInjected }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let attempted = false;
|
let attempted = false;
|
||||||
const dial = (allowRetry) => {
|
const dial = (allowRetry) => {
|
||||||
@@ -42,6 +42,7 @@ function connectUpstream({ host, port, username, privateKey, onHostKey, uid, jus
|
|||||||
})
|
})
|
||||||
.connect({
|
.connect({
|
||||||
host, port, username, privateKey,
|
host, port, username, privateKey,
|
||||||
|
certificates: cert ? [cert] : undefined,
|
||||||
readyTimeout: (conf.ssh && conf.ssh.connectTimeoutMs) || 10000,
|
readyTimeout: (conf.ssh && conf.ssh.connectTimeoutMs) || 10000,
|
||||||
keepaliveInterval: 15000,
|
keepaliveInterval: 15000,
|
||||||
hostVerifier: (key) => {
|
hostVerifier: (key) => {
|
||||||
|
|||||||
@@ -129,14 +129,26 @@ async function resolveAndConnect(state, record, { onHostKey } = {}) {
|
|||||||
await record.patch({ targetSlug: host ? host.slug : 'raw-ip', targetAddr: endpoint.address, targetPort: endpoint.port });
|
await record.patch({ targetSlug: host ? host.slug : 'raw-ip', targetAddr: endpoint.address, targetPort: endpoint.port });
|
||||||
|
|
||||||
let justInjected = false;
|
let justInjected = false;
|
||||||
try { justInjected = await ensureKeyInjected(state.user, JUMP_KEYS.publicLine); }
|
let cert;
|
||||||
catch (err) { throw fail('key-inject-failed', err.message, host ? host.slug : undefined); }
|
const usePki = conf.ssh && conf.ssh.pki && conf.ssh.pki.enabled;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (usePki) {
|
||||||
|
const { getSignedCert } = require('../utils/vault_cert');
|
||||||
|
cert = await getSignedCert(JUMP_KEYS.publicLine, state.uid);
|
||||||
|
} else {
|
||||||
|
justInjected = await ensureKeyInjected(state.user, JUMP_KEYS.publicLine);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const failType = usePki ? 'pki-cert-failed' : 'key-inject-failed';
|
||||||
|
throw fail(failType, err.message, host ? host.slug : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
let upstream;
|
let upstream;
|
||||||
try {
|
try {
|
||||||
upstream = await connectUpstream({
|
upstream = await connectUpstream({
|
||||||
host: endpoint.address, port: endpoint.port,
|
host: endpoint.address, port: endpoint.port,
|
||||||
username: state.uid, privateKey: JUMP_KEYS.clientKey,
|
username: state.uid, privateKey: JUMP_KEYS.clientKey, cert,
|
||||||
uid: state.uid, justInjected, onHostKey,
|
uid: state.uid, justInjected, onHostKey,
|
||||||
});
|
});
|
||||||
} catch (err) { throw fail('upstream-unreachable', err.message, host ? host.slug : undefined); }
|
} catch (err) { throw fail('upstream-unreachable', err.message, host ? host.slug : undefined); }
|
||||||
@@ -228,14 +240,26 @@ async function runTuiSession(session, client, state) {
|
|||||||
await record.patch({ targetSlug: tui.host.slug, targetAddr: endpoint.address, targetPort: endpoint.port });
|
await record.patch({ targetSlug: tui.host.slug, targetAddr: endpoint.address, targetPort: endpoint.port });
|
||||||
|
|
||||||
let justInjected = false;
|
let justInjected = false;
|
||||||
try { justInjected = await ensureKeyInjected(state.user, JUMP_KEYS.publicLine); }
|
let cert;
|
||||||
catch (err) { return finishFail('key-inject-failed', err.message, tui.host.slug); }
|
const usePki = conf.ssh && conf.ssh.pki && conf.ssh.pki.enabled;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (usePki) {
|
||||||
|
const { getSignedCert } = require('../utils/vault_cert');
|
||||||
|
cert = await getSignedCert(JUMP_KEYS.publicLine, state.uid);
|
||||||
|
} else {
|
||||||
|
justInjected = await ensureKeyInjected(state.user, JUMP_KEYS.publicLine);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const failType = usePki ? 'pki-cert-failed' : 'key-inject-failed';
|
||||||
|
return finishFail(failType, err.message, tui.host.slug);
|
||||||
|
}
|
||||||
|
|
||||||
let upstream;
|
let upstream;
|
||||||
try {
|
try {
|
||||||
upstream = await connectUpstream({
|
upstream = await connectUpstream({
|
||||||
host: endpoint.address, port: endpoint.port,
|
host: endpoint.address, port: endpoint.port,
|
||||||
username: state.uid, privateKey: JUMP_KEYS.clientKey,
|
username: state.uid, privateKey: JUMP_KEYS.clientKey, cert,
|
||||||
uid: state.uid, justInjected, onHostKey: (fp) => record.patch({ hostKeyFp: fp }),
|
uid: state.uid, justInjected, onHostKey: (fp) => record.patch({ hostKeyFp: fp }),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const conf = require('@simpleworkjs/conf');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a signed SSH certificate from the SSO Manager's OpenBao/Vault proxy.
|
||||||
|
*
|
||||||
|
* @param {string} publicKey - The jump host's public key (e.g. 'ssh-rsa AAAAB3...')
|
||||||
|
* @param {string} targetUid - The username the cert should be valid for
|
||||||
|
* @returns {Promise<string>} - The signed SSH certificate
|
||||||
|
*/
|
||||||
|
async function getSignedCert(publicKey, targetUid) {
|
||||||
|
const sso = conf.sso || {};
|
||||||
|
const pkiConfig = conf.ssh?.pki || {};
|
||||||
|
|
||||||
|
const vaultRole = pkiConfig.role || 'jump-host-role';
|
||||||
|
const endpoint = `${sso.url}/api/vault/ssh/sign/${vaultRole}`;
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${sso.apiToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
public_key: publicKey,
|
||||||
|
valid_principals: targetUid
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text().catch(() => '');
|
||||||
|
throw new Error(`Failed to sign SSH cert (status ${response.status}): ${errText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.data || !data.data.signed_key) {
|
||||||
|
throw new Error('Vault response missing signed_key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data.signed_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getSignedCert };
|
||||||
Reference in New Issue
Block a user