394 lines
10 KiB
TypeScript
394 lines
10 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
assertLinuxAccountName,
|
|
assertAllowedSystemUserShell,
|
|
assertCredentialMutationAllowed,
|
|
assertSystemUserPassword,
|
|
getAuthorizedKeysRevision,
|
|
getPrivilegedSystemGroups,
|
|
hasRequiredPrivilegeAcknowledgement,
|
|
normalizeOpenSshPublicKey,
|
|
parseAuthorizedKeyLine,
|
|
parseSystemUserPasswordStatus,
|
|
SystemAccountConflictError,
|
|
SystemAccountInputError,
|
|
validateSystemGroupDeletePolicy,
|
|
validateSystemUserDeletePolicy,
|
|
validateSystemUserUpdatePolicy,
|
|
} from "../../src/server/services/ssh-services/internal/system-account-security";
|
|
|
|
function createEd25519PublicKey(comment = "test device") {
|
|
const type = Buffer.from("ssh-ed25519");
|
|
const key = Buffer.alloc(32, 7);
|
|
const blob = Buffer.alloc(4 + type.length + 4 + key.length);
|
|
blob.writeUInt32BE(type.length, 0);
|
|
type.copy(blob, 4);
|
|
blob.writeUInt32BE(key.length, 4 + type.length);
|
|
key.copy(blob, 8 + type.length);
|
|
return `ssh-ed25519 ${blob.toString("base64")} ${comment}`;
|
|
}
|
|
|
|
test("system account names accept conservative Linux-safe identifiers", () => {
|
|
for (const name of ["deploy", "www-data", "_service", "app_2026"]) {
|
|
assert.doesNotThrow(() => assertLinuxAccountName(name, "User"));
|
|
}
|
|
});
|
|
|
|
test("system account names reject shell syntax, traversal, uppercase, and oversized input", () => {
|
|
for (const name of [
|
|
"Deploy",
|
|
"-root",
|
|
"../root",
|
|
"user;reboot",
|
|
"user name",
|
|
"a".repeat(33),
|
|
]) {
|
|
assert.throws(
|
|
() => assertLinuxAccountName(name, "User"),
|
|
SystemAccountInputError,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("privileged group assignments require explicit acknowledgement", () => {
|
|
assert.deepEqual(getPrivilegedSystemGroups(["www-data", "docker", "sudo"]), [
|
|
"docker",
|
|
"sudo",
|
|
]);
|
|
assert.equal(
|
|
hasRequiredPrivilegeAcknowledgement(["www-data"], false),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
hasRequiredPrivilegeAcknowledgement(["docker"], false),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
hasRequiredPrivilegeAcknowledgement(["docker", "root"], true),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("system user shell changes use a fixed allowlist", () => {
|
|
assert.doesNotThrow(() => assertAllowedSystemUserShell("/bin/bash"));
|
|
assert.doesNotThrow(() =>
|
|
assertAllowedSystemUserShell("/usr/sbin/nologin"),
|
|
);
|
|
assert.throws(
|
|
() => assertAllowedSystemUserShell("/tmp/custom-shell"),
|
|
SystemAccountInputError,
|
|
);
|
|
});
|
|
|
|
const regularUpdate = {
|
|
username: "deploy",
|
|
uid: 1001,
|
|
serverUsername: "operator",
|
|
currentGroups: ["deploy", "www-data"],
|
|
primaryGroup: "deploy",
|
|
currentShell: "/bin/bash",
|
|
desiredGroups: ["deploy", "www-data", "docker"],
|
|
desiredShell: "/bin/sh",
|
|
expectedGroups: ["www-data", "deploy"],
|
|
expectedShell: "/bin/bash",
|
|
acknowledgePrivilegedGroups: true,
|
|
};
|
|
|
|
test("regular system users can update shell and exact supplementary groups", () => {
|
|
assert.deepEqual(validateSystemUserUpdatePolicy(regularUpdate), {
|
|
isSshUser: false,
|
|
addedGroups: ["docker"],
|
|
removedGroups: [],
|
|
shellChanged: true,
|
|
});
|
|
});
|
|
|
|
test("system user updates reject root, primary-group removal, and stale snapshots", () => {
|
|
assert.throws(
|
|
() => validateSystemUserUpdatePolicy({ ...regularUpdate, uid: 0 }),
|
|
/root account is protected/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserUpdatePolicy({
|
|
...regularUpdate,
|
|
desiredGroups: ["www-data"],
|
|
}),
|
|
/Primary group deploy cannot be removed/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserUpdatePolicy({
|
|
...regularUpdate,
|
|
expectedGroups: ["deploy"],
|
|
}),
|
|
SystemAccountConflictError,
|
|
);
|
|
});
|
|
|
|
test("active SSH login updates are add-only and cannot change shell", () => {
|
|
const sshUpdate = {
|
|
...regularUpdate,
|
|
username: "operator",
|
|
serverUsername: "operator",
|
|
currentGroups: ["operator", "sudo"],
|
|
primaryGroup: "operator",
|
|
desiredGroups: ["operator", "sudo", "www-data"],
|
|
desiredShell: "/bin/bash",
|
|
expectedGroups: ["operator", "sudo"],
|
|
acknowledgePrivilegedGroups: false,
|
|
};
|
|
|
|
assert.deepEqual(validateSystemUserUpdatePolicy(sshUpdate), {
|
|
isSshUser: true,
|
|
addedGroups: ["www-data"],
|
|
removedGroups: [],
|
|
shellChanged: false,
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserUpdatePolicy({
|
|
...sshUpdate,
|
|
desiredGroups: ["operator"],
|
|
}),
|
|
/Existing groups cannot be removed/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserUpdatePolicy({
|
|
...sshUpdate,
|
|
desiredShell: "/bin/sh",
|
|
}),
|
|
/SSH login shell cannot be changed/,
|
|
);
|
|
});
|
|
|
|
test("new privileged memberships require acknowledgement during updates", () => {
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserUpdatePolicy({
|
|
...regularUpdate,
|
|
acknowledgePrivilegedGroups: false,
|
|
}),
|
|
/Explicit confirmation is required for privileged groups: docker/,
|
|
);
|
|
});
|
|
|
|
test("password validation rejects weak, username-derived, and control-character values", () => {
|
|
assert.doesNotThrow(() =>
|
|
assertSystemUserPassword("deploy", "correct-horse-battery-staple"),
|
|
);
|
|
assert.throws(() => assertSystemUserPassword("deploy", "short"), /12/);
|
|
assert.throws(
|
|
() => assertSystemUserPassword("deploy", "Deploy-password-2026"),
|
|
/username/i,
|
|
);
|
|
assert.throws(
|
|
() => assertSystemUserPassword("deploy", "valid-password\nsecret"),
|
|
/control characters/i,
|
|
);
|
|
});
|
|
|
|
test("OpenSSH public keys are normalized, fingerprinted, and parsed without exposing private material", () => {
|
|
const key = createEd25519PublicKey();
|
|
const normalized = normalizeOpenSshPublicKey(key, "Developer laptop");
|
|
assert.equal(normalized.keyType, "ssh-ed25519");
|
|
assert.match(normalized.fingerprint, /^SHA256:[A-Za-z0-9+/]{43}$/);
|
|
assert.equal(normalized.comment, "Developer laptop");
|
|
assert.deepEqual(parseAuthorizedKeyLine(normalized.line), normalized);
|
|
assert.equal(getAuthorizedKeysRevision(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
|
assert.throws(
|
|
() => normalizeOpenSshPublicKey(key.replace("ssh-ed25519", "ssh-rsa")),
|
|
/does not match/i,
|
|
);
|
|
});
|
|
|
|
test("password status codes are reduced to safe account states", () => {
|
|
assert.equal(parseSystemUserPasswordStatus("P"), "set");
|
|
assert.equal(parseSystemUserPasswordStatus("L"), "locked");
|
|
assert.equal(parseSystemUserPasswordStatus("NP"), "not-set");
|
|
assert.equal(parseSystemUserPasswordStatus("unexpected"), "unknown");
|
|
});
|
|
|
|
test("active Doktainer credentials cannot be replaced or revoked", () => {
|
|
const base = {
|
|
username: "operator",
|
|
uid: 1000,
|
|
serverUsername: "operator",
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
assertCredentialMutationAllowed({
|
|
...base,
|
|
serverAuthType: "PASSWORD",
|
|
action: "password-set",
|
|
}),
|
|
/active Doktainer SSH connection/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
assertCredentialMutationAllowed({
|
|
...base,
|
|
serverAuthType: "SSH_KEY",
|
|
action: "key-revoke",
|
|
}),
|
|
/reconnect verification/i,
|
|
);
|
|
assert.doesNotThrow(() =>
|
|
assertCredentialMutationAllowed({
|
|
...base,
|
|
serverAuthType: "SSH_KEY",
|
|
action: "key-add",
|
|
}),
|
|
);
|
|
});
|
|
|
|
const regularDelete = {
|
|
username: "deploy",
|
|
uid: 1001,
|
|
gid: 1001,
|
|
home: "/home/deploy",
|
|
shell: "/bin/bash",
|
|
uidMin: 1000,
|
|
serverUsername: "operator",
|
|
expectedUid: 1001,
|
|
expectedGid: 1001,
|
|
expectedHome: "/home/deploy",
|
|
expectedShell: "/bin/bash",
|
|
confirmation: "deploy",
|
|
activeProcessCount: 0,
|
|
removeHome: false,
|
|
};
|
|
|
|
test("regular users can be approved for deletion with stale-state guards", () => {
|
|
assert.deepEqual(validateSystemUserDeletePolicy(regularDelete), {
|
|
username: "deploy",
|
|
uid: 1001,
|
|
home: "/home/deploy",
|
|
removeHome: false,
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserDeletePolicy({
|
|
...regularDelete,
|
|
expectedShell: "/bin/sh",
|
|
}),
|
|
SystemAccountConflictError,
|
|
);
|
|
});
|
|
|
|
test("user deletion protects root, system users, active SSH identity, and active processes", () => {
|
|
assert.throws(
|
|
() => validateSystemUserDeletePolicy({ ...regularDelete, uid: 0 }),
|
|
/root account is protected/i,
|
|
);
|
|
assert.throws(
|
|
() => validateSystemUserDeletePolicy({ ...regularDelete, uid: 999 }),
|
|
/system users/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserDeletePolicy({
|
|
...regularDelete,
|
|
serverUsername: "deploy",
|
|
}),
|
|
/active Doktainer SSH connection/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserDeletePolicy({
|
|
...regularDelete,
|
|
activeProcessCount: 2,
|
|
}),
|
|
/active processes/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserDeletePolicy({
|
|
...regularDelete,
|
|
confirmation: "other-user",
|
|
}),
|
|
/exact username/i,
|
|
);
|
|
});
|
|
|
|
test("recursive user deletion is restricted to the standard account home", () => {
|
|
assert.doesNotThrow(() =>
|
|
validateSystemUserDeletePolicy({ ...regularDelete, removeHome: true }),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemUserDeletePolicy({
|
|
...regularDelete,
|
|
home: "/srv/shared",
|
|
expectedHome: "/srv/shared",
|
|
removeHome: true,
|
|
}),
|
|
/standard \/home/i,
|
|
);
|
|
});
|
|
|
|
const regularGroupDelete = {
|
|
groupName: "developers",
|
|
gid: 1002,
|
|
gidMin: 1000,
|
|
members: [] as string[],
|
|
primaryUsers: [] as string[],
|
|
expectedGid: 1002,
|
|
expectedMembers: [] as string[],
|
|
expectedPrimaryUsers: [] as string[],
|
|
confirmation: "developers",
|
|
};
|
|
|
|
test("only empty non-system groups can be approved for deletion", () => {
|
|
assert.doesNotThrow(() =>
|
|
validateSystemGroupDeletePolicy(regularGroupDelete),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemGroupDeletePolicy({
|
|
...regularGroupDelete,
|
|
gid: 999,
|
|
expectedGid: 999,
|
|
}),
|
|
/system and privileged/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemGroupDeletePolicy({
|
|
...regularGroupDelete,
|
|
groupName: "docker",
|
|
confirmation: "docker",
|
|
}),
|
|
/system and privileged/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemGroupDeletePolicy({
|
|
...regularGroupDelete,
|
|
members: ["deploy"],
|
|
expectedMembers: ["deploy"],
|
|
}),
|
|
/members first/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemGroupDeletePolicy({
|
|
...regularGroupDelete,
|
|
primaryUsers: ["deploy"],
|
|
expectedPrimaryUsers: ["deploy"],
|
|
}),
|
|
/primary for/i,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateSystemGroupDeletePolicy({
|
|
...regularGroupDelete,
|
|
expectedMembers: ["stale-member"],
|
|
}),
|
|
SystemAccountConflictError,
|
|
);
|
|
});
|