diff --git a/root-ubuntu.sh b/root-ubuntu.sh index 1efe336..d47c436 100755 --- a/root-ubuntu.sh +++ b/root-ubuntu.sh @@ -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)" @@ -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 '^$')" @@ -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 diff --git a/test/root-ubuntu.test.ts b/test/root-ubuntu.test.ts index 36630d2..6687694 100644 --- a/test/root-ubuntu.test.ts +++ b/test/root-ubuntu.test.ts @@ -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 { @@ -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'];