Skip to content

Commit

Permalink
shadow: Adjust all deployments
Browse files Browse the repository at this point in the history
It was pointed out that in the previous change here we missed
the fact that the previous deployments were accessible.

- Move the logic into Rust, adding unit tests
- Change the code to iterate over all deployments
- Add an integration test too

Note: A likely future enhancement here will be to finally
deny unprivileged access to non-default roots; cc
ostreedev/ostree#3211
  • Loading branch information
cgwalters committed Apr 12, 2024
1 parent 45a34cf commit 2d51f18
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 5 deletions.
2 changes: 1 addition & 1 deletion rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ mod normalization;
mod origin;
mod ostree_prepareroot;
pub(crate) use self::origin::*;
mod passwd;
pub mod passwd;
use passwd::*;
mod console_progress;
pub(crate) use self::console_progress::*;
Expand Down
1 change: 1 addition & 0 deletions rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async fn inner_async_main(args: Vec<String>) -> Result<i32> {
match *arg {
// Add custom Rust commands here, and also in `libmain.cxx` if user-visible.
"countme" => rpmostree_rust::countme::entrypoint(args).map(|_| 0),
"fix-shadow-perms" => rpmostree_rust::passwd::fix_shadow_perms_entrypoint(args).map(|_| 0),
"cliwrap" => rpmostree_rust::cliwrap::entrypoint(args).map(|_| 0),
// A hidden wrapper to intercept some binaries in RPM scriptlets.
"scriptlet-intercept" => builtins::scriptlet_intercept::entrypoint(args).map(|_| 0),
Expand Down
122 changes: 122 additions & 0 deletions rust/src/passwd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const DEFAULT_MODE: u32 = 0o644;
static DEFAULT_PERMS: Lazy<Permissions> = Lazy::new(|| Permissions::from_mode(DEFAULT_MODE));
static PWGRP_SHADOW_FILES: &[&str] = &["shadow", "gshadow", "subuid", "subgid"];
static USRLIB_PWGRP_FILES: &[&str] = &["passwd", "group"];
// This stamp file signals the original fix which only changed the booted deployment
const SHADOW_MODE_FIXED_STAMP_OLD: &str = "etc/.rpm-ostree-shadow-mode-fixed.stamp";
// And this one is written by the newer logic that changes all deployments
const SHADOW_MODE_FIXED_STAMP: &str = "etc/.rpm-ostree-shadow-mode-fixed2.stamp";

// Lock/backup files that should not be in the base commit (TODO fix).
static PWGRP_LOCK_AND_BACKUP_FILES: &[&str] = &[
Expand Down Expand Up @@ -363,6 +367,84 @@ impl PasswdKind {
}
}

/// Due to a prior bug, the build system had some deployments with a world-readable
/// shadow file. This fixes a given deployment.
#[context("Fixing shadow permissions")]
pub(crate) fn fix_shadow_perms_in_root(root: &Dir) -> Result<bool> {
let zero_perms = Permissions::from_mode(0);
let mut changed = false;
for path in ["etc/shadow", "etc/shadow-", "etc/gshadow", "etc/gshadow-"] {
let metadata = if let Some(meta) = root
.symlink_metadata_optional(path)
.context("Querying metadata")?
{
meta
} else {
tracing::debug!("No path {path}");
continue;
};
let mode = metadata.mode() & !libc::S_IFMT;
// Don't touch the file if it's already correct
if mode == 0 {
continue;
}
let f = root.open(path).with_context(|| format!("Opening {path}"))?;
f.set_permissions(zero_perms.clone())
.context("chmod to zero")?;
println!("Adjusted mode for {path}");
changed = true;
}
// Write our stamp file
root.write(SHADOW_MODE_FIXED_STAMP, "")
.context(SHADOW_MODE_FIXED_STAMP)?;
// And clean up the old one
root.remove_file_optional(SHADOW_MODE_FIXED_STAMP_OLD)
.with_context(|| format!("Removing old {SHADOW_MODE_FIXED_STAMP_OLD}"))?;
Ok(changed)
}

/// Due to a prior bug, the build system had some deployments with a world-readable
/// shadow file. This fixes all deployments.
pub(crate) fn fix_shadow_perms_in_sysroot(sysroot: &ostree::Sysroot) -> Result<bool> {
let deployments = sysroot.deployments();
// TODO add a nicer api for this to ostree-rs
let sysroot_fd =
Dir::reopen_dir(unsafe { &std::os::fd::BorrowedFd::borrow_raw(sysroot.fd()) })?;
let mut changed = false;
for deployment in deployments {
let path = sysroot.deployment_dirpath(&deployment);
let dir = sysroot_fd.open_dir(&path)?;
if fix_shadow_perms_in_root(&dir)? {
println!(
"Adjusted shadow files in deployment index={} {}.{}",
deployment.index(),
deployment.csum(),
deployment.bootserial()
);
changed = true;
}
}
Ok(changed)
}

/// The main entrypoint for updating /etc/{,g}shadow permissions across
/// all deployments.
pub fn fix_shadow_perms_entrypoint(_args: &[&str]) -> Result<()> {
let cancellable = gio::Cancellable::NONE;
let sysroot = ostree::Sysroot::new_default();
sysroot.set_mount_namespace_in_use();
sysroot.lock()?;
sysroot.load(cancellable)?;
let changed = fix_shadow_perms_in_sysroot(&sysroot)?;
if changed {
// We already printed per deployment, so this one is just
// a debug-level log.
tracing::debug!("Updated shadow/gshadow permissions");
}
sysroot.unlock();
Ok(())
}

// This function writes the static passwd/group data from the treefile to the
// target root filesystem.
fn write_data_from_treefile(
Expand Down Expand Up @@ -1070,3 +1152,43 @@ impl PasswdEntries {
Ok(())
}
}

#[test]
fn test_shadow_perms() -> Result<()> {
let root = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
root.create_dir("etc")?;
root.write("etc/shadow", "some shadow")?;
root.write("etc/gshadow", "some gshadow")?;
root.set_permissions("etc/gshadow", Permissions::from_mode(0))?;

assert!(fix_shadow_perms_in_root(root)?);
assert!(!root.try_exists(SHADOW_MODE_FIXED_STAMP_OLD)?);
assert!(!root.try_exists(SHADOW_MODE_FIXED_STAMP)?);
// Verify idempotence
assert!(!fix_shadow_perms_in_root(root)?);
assert!(!root.try_exists(SHADOW_MODE_FIXED_STAMP_OLD)?);
assert!(root.try_exists(SHADOW_MODE_FIXED_STAMP)?);

Ok(())
}

#[test]
/// Verify the scenario of updating from a previously fixed root
fn test_shadow_perms_from_orig_fix() -> Result<()> {
let root = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
root.create_dir("etc")?;
root.write("etc/shadow", "some shadow")?;
root.set_permissions("etc/shadow", Permissions::from_mode(0))?;
root.write("etc/gshadow", "some gshadow")?;
root.set_permissions("etc/gshadow", Permissions::from_mode(0))?;
// Write the original stamp file
root.write(SHADOW_MODE_FIXED_STAMP_OLD, "")?;

// No changes
assert!(!fix_shadow_perms_in_root(root)?);
// Except we should have updated to the new stamp file
assert!(!root.try_exists(SHADOW_MODE_FIXED_STAMP_OLD)?);
assert!(root.try_exists(SHADOW_MODE_FIXED_STAMP)?);

Ok(())
}
12 changes: 8 additions & 4 deletions src/daemon/rpm-ostree-fix-shadow-mode.service
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
# This makes sure to fix permissions on systems that were deployed with the wrong permissions.
Description=Update permissions for /etc/shadow
Documentation=https://github.com/coreos/rpm-ostree-ghsa-2m76-cwhg-7wv6
ConditionPathExists=!/etc/.rpm-ostree-shadow-mode-fixed.stamp
# This new stamp file is written by the Rust code, and obsoletes
# the old /etc/.rpm-ostree-shadow-mode-fixed.stamp
ConditionPathExists=!/etc/.rpm-ostree-shadow-mode-fixed2.stamp
ConditionPathExists=/run/ostree-booted
# Because we read the sysroot
RequiresMountsFor=/boot
# Make sure this is started before any unprivileged (interactive) user has access to the system.
Before=systemd-user-sessions.service

[Service]
Type=oneshot
ExecStart=chmod --verbose 0000 /etc/shadow /etc/gshadow
ExecStart=-chmod --verbose 0000 /etc/shadow- /etc/gshadow-
ExecStart=touch /etc/.rpm-ostree-shadow-mode-fixed.stamp
ExecStart=rpm-ostree fix-shadow-perms
RemainAfterExit=yes
# So we can remount /sysroot writable in our own namespace
MountFlags=slave

[Install]
WantedBy=multi-user.target
80 changes: 80 additions & 0 deletions tests/kolainst/destructive/shadow
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash
#
# Copyright (C) 2024 Red Hat Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

set -euo pipefail

. ${KOLA_EXT_DATA}/libtest.sh

set -x

cd $(mktemp -d)

service=rpm-ostree-fix-shadow-mode.service
stamp=/etc/.rpm-ostree-shadow-mode-fixed2.stamp

case "${AUTOPKGTEST_REBOOT_MARK:-}" in
"")

libtest_prepare_fully_offline
libtest_enable_repover 0

systemctl status ${service} || true
rm -vf /etc/.rpm-ostree-shadow-mode*
chmod 0644 /etc/gshadow

# Verify running the service once fixes things
systemctl restart $service
assert_has_file "${stamp}"
assert_streq "$(stat -c '%f' /etc/gshadow)" 8000

# Now *undo* the fix, so that the current (then old) deployment
# is broken still, and ensure after reboot that it's fixed
# in both.

chmod 0644 /etc/gshadow
rm -vf /etc/.rpm-ostree*

booted_commit=$(rpm-ostree status --json | jq -r '.deployments[0].checksum')
ostree refs ${booted_commit} --create vmcheck2
rpm-ostree rebase :vmcheck2

/tmp/autopkgtest-reboot "1"
;;
"1")

systemctl status $service
assert_has_file "${stamp}"

verified=0
for f in $(ls /ostree/deploy/*/deploy/*/etc/{,g}shadow{,-}); do
verified=$(($verified + 1))
assert_streq "$(stat -c '%f' $f)" 8000
echo "ok ${f}"
done
assert_streq "$verified" 8

journalctl -b -u $service --grep="Adjusted shadow files in deployment" | tee out.txt
assert_streq "$(wc -l < out.txt)" 2

echo "ok shadow"

;;
*) echo "unexpected mark: ${AUTOPKGTEST_REBOOT_MARK}"; exit 1;;

esac

0 comments on commit 2d51f18

Please sign in to comment.