Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions root-ubuntu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2014,13 +2014,21 @@ _confine_sysctl_floor() {
local key="$1" floor="$2" have
have="$(sysctl -n "$key" 2>/dev/null)" || return 1
[[ "$have" =~ ^-?[0-9]+$ ]] || return 1
# At the floor BECAUSE of our own file is not "already at the floor": the
# line stays. Otherwise the second run sees the value the first run raised,
# writes nothing, deletes the file, and the box boots without the hardening.
if (( have == floor )) && grep -qxF "$key = $floor" "${CONFINE_SYSCTL_CONF:-}" 2>/dev/null; then
printf '%s = %s\n' "$key" "$floor"
return 0
fi
(( have >= floor )) && return 1
printf '%s = %s\n' "$key" "$floor"
return 0
}

CONFINE_SYSCTL_CONF=/etc/sysctl.d/62-profullstack-confine.conf
configure_confine_sysctl() {
local conf=/etc/sysctl.d/62-profullstack-confine.conf body="" dumpable
local conf="$CONFINE_SYSCTL_CONF" body="" dumpable

if [[ "$CONFINE_SYSCTL" != 1 ]]; then
info "confine: sysctl hardening off (CONFINE_SYSCTL=0)"
Expand Down Expand Up @@ -2052,7 +2060,11 @@ configure_confine_sysctl() {
# have left a box sitting on 2 alone and called it hardened. It is an exact
# value instead, and 0 can never be a loosening of anything.
dumpable="$(sysctl -n fs.suid_dumpable 2>/dev/null)"
[[ "$dumpable" =~ ^[0-9]+$ && "$dumpable" != 0 ]] && body+='fs.suid_dumpable = 0'$'\n'
# Same rule as the floors: a 0 that our own file set is kept, not dropped.
if [[ "$dumpable" =~ ^[0-9]+$ && "$dumpable" != 0 ]] \
|| { [[ "$dumpable" == 0 ]] && grep -qxF 'fs.suid_dumpable = 0' "$conf" 2>/dev/null; }; then
body+='fs.suid_dumpable = 0'$'\n'
fi

body="$(printf '%s' "$body" | grep -v '^$')"

Expand Down Expand Up @@ -3670,7 +3682,13 @@ resolve_dotfiles() {
DOTFILES_DIR=""
fi

if looks_like_dotfiles "$SCRIPT_DIR"; then
# ...but only a CHECKOUT. The documented way to run this is from /root
# (curl -O, then ./root-ubuntu.sh), and after the first run /root has a
# .zshrc of its own -- oh-my-zsh puts one there. Taken at face value that
# made the second run treat root's home as the team's dotfiles and copy
# root's shell config into every account. A dotfiles repo is a git
# checkout; a home directory is not.
if looks_like_dotfiles "$SCRIPT_DIR" && [[ -e "$SCRIPT_DIR/.git" ]]; then
DOTFILES_DIR="$SCRIPT_DIR"
info "dotfiles: $DOTFILES_DIR (running from inside them)"
return 0
Expand Down
48 changes: 47 additions & 1 deletion test/root-ubuntu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@ describe('the user confine', () => {
-p|*) return 0 ;;
esac
}
eval "$(declare -f configure_confine_sysctl | sed 's#/etc/sysctl.d/62-profullstack-confine.conf#${dir}/out.conf#g')"
CONFINE_SYSCTL_CONF=${dir}/out.conf
configure_confine_sysctl`,
);
try {
Expand All @@ -1054,6 +1054,52 @@ describe('the user confine', () => {
});
});

describe('second runs keep what the first run wrote', () => {
it('keeps a sysctl floor its own file put there, instead of deleting the file on run two', () => {
const run = (have: string, file: string) =>
shell(
['_confine_sysctl_floor'],
`sysctl() { echo ${have}; }; CONFINE_SYSCTL_CONF=$(mktemp); printf '${file}' >"$CONFINE_SYSCTL_CONF"
_confine_sysctl_floor kernel.kptr_restrict 2 || echo none`,
);
expect(run('1', '')).toBe('kernel.kptr_restrict = 2');
// at the floor because of our own file: the line stays
expect(run('2', 'kernel.kptr_restrict = 2\\n')).toBe('kernel.kptr_restrict = 2');
// at the floor on its own: nothing to write
expect(run('2', '')).toBe('none');
// stricter than the floor: never written back down
expect(run('3', 'kernel.kptr_restrict = 2\\n')).toBe('none');
});

it('keeps fs.suid_dumpable = 0 when its own file set it', () => {
const dir = mkdtempSync(join(tmpdir(), 'root-ubuntu-suid2-'));
writeFileSync(join(dir, 'out.conf'), 'fs.suid_dumpable = 0\n');
shell(
['_confine_sysctl_floor', 'configure_confine_sysctl', 'write_if_changed', 'file_sha'],
`CONFINE_SYSCTL=1; note() { :; }; info() { :; }; warn() { :; }
sysctl() { case "\${2:-}" in fs.suid_dumpable) echo 0 ;; -p|*) echo 99 ;; esac; }
CONFINE_SYSCTL_CONF=${dir}/out.conf
configure_confine_sysctl`,
);
expect(readFileSync(join(dir, 'out.conf'), 'utf8')).toContain('fs.suid_dumpable = 0');
});

it("does not mistake root's home for a dotfiles checkout, but still takes a real one", () => {
const resolve = (dir: string) =>
shell(
['looks_like_dotfiles', 'resolve_dotfiles'],
`info() { :; }; warn() { :; }; SKIP_DOTFILES=0; DOTFILES_DIR=; DOTFILES_REPO=; SCRIPT_DIR=${dir}; resolve_dotfiles; echo "[$DOTFILES_DIR]"`,
);
const home = mkdtempSync(join(tmpdir(), 'home-'));
writeFileSync(join(home, '.zshrc'), '# root after run one\n');
expect(resolve(home)).toBe('[]');
const repo = mkdtempSync(join(tmpdir(), 'dotfiles-'));
writeFileSync(join(repo, '.zshrc'), '# the team dotfiles\n');
execFileSync('git', ['init', '-q', repo]);
expect(resolve(repo)).toBe(`[${repo}]`);
});
});

describe('_confine_fstab_proc', () => {
const FNS = ['_confine_fstab_proc'];

Expand Down
Loading