Files
doktainer-upstream/tests/backend/system-account-security.test.ts
T

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,
);
});