#! /bin/bash # # This script attempts to fix the following issue: # https://github.com/wayblueorg/wayblue/issues/89 # More specifically, it does the following: # * Iterates /etc/shadow and /etc/gshadow; and # * For every entry that cannot be getent'd, delete it # # This script should be invoked before systemd-sysusers on system boot # # The reason for this is as follows: # At time of writing, using rpm-ostree to build OCI container images fails to # update /usr/lib/passwd and /usr/lib/group, instead dropping items in # /usr/lib/sysusers.d for systemd-sysusers to process at boot time. This would # fine under normal circumstances. # # HOWEVER. If you are coming from a distro that had entries in those /usr/lib # files for that users/group, you will have entries in /etc/{,g}shadow for said # users/groups. # # If an entry is present in /etc/shadow or /etc/gshadow that matches an object # that systemd-sysusers is trying to add, it will fail and then abort further # object processing. Thus, we remove objects that cannot be looked up, assuming # that the cause is this disparity and that it will be smoothed out when # systemd-sysusers next runs # set -euo pipefail # Iterate over each file we're interested in for file in /etc/shadow /etc/gshadow; do # Prelim check for zero-byte files (shouldn't proc) if ! [ -s "$file" ]; then echo "File is missing or empty: $file" continue fi # Prelim check to ensure we can read the file if ! [ -r "$file" ]; then echo "Unable to read file: $file" continue fi # Prelim checks succeeded, move forward echo "Parsing $file for junk" # Read each line in the file to iterate over it while read line; do # This should never happen, but if for some reason we get an empty line, # continue [ -z "$line" ] && continue # Per shadow(5), we are guaranteed that all characters leading up to the # first colon are the user's/group's name. To that end, we'll do a bash # string substitution to extract that first column. name="${line%%:*}" # Swith case here for what ents we care about. I don't think the array is # strictly required but it affords us flexibility for zero cost so whatever. ents=() case "$file" in */shadow) ents=("passwd") ;; */gshadow) ents=("group") ;; *) echo "Unknown file to parse for junk: $file" exit 101 ;; esac # Now, we use getent to find a match for the shadow entry. It's at this point # that one might ask why we're doing this instead of using grpck or something # The answer is that those aren't nss-aware, whereas getent is. /etc/shadow # and /etc/gshadow can have entries for things like passwords and group # membership for groups that live in /usr/lib/passwd and /usr/lib/group, # but grpck isn't aware of those and will obliterate their data from the # shadow files if we're not careful. # # Thus, we use getent to parse through nsswitch.conf intelligently and tell # us whether this object we're operating on is found elsewhere in the OS matched_ent="" for ent in "${ents[@]}"; do [ -n "$matched_ent" ] && break # We have to do || true here because getent exits nonzero if it fails to # find an entity. We need to store its output so we can analyze the status # of this command outside the loop -- we can't just "if ! getent" result="$(getent "$ent" "$name" || true)" [ -n "$result" ] && matched_ent="$result" done if [ -n "$matched_ent" ]; then # getent found a matching entity and we don't care about this entry # Just continue down the line continue else # If we're at this point in the code path, we now know that we for-sure are # operating on an entry that will cause systemd-sysusers to bail out # on invocation. We are thus going to remove it. echo "Analyzing broken entity: $name" # First, we're going to pattern match the username against the systemd # common core username regex. If this fails to match, we bail. I was unable # to find a Fedora username that didn't match this but it's best to have # this type of safety -- you never know what might happen. # https://systemd.io/USER_NAMES/ if ! [[ $name =~ ^[a-z][a-z0-9-]{0,30}$ ]]; then echo "Not touching nonconformant name: $name" continue fi # We've succeeded in all our checks and for sure have a username loaded # that isn't going to cause our regex to explode in terrifying ways. # We're now going to load sed up and fire it at the shadowfile, making # a backup along the way echo "Removing from $file: $name" sed --in-place=- \ "/^$name:/d" \ "$file" fi # Not useless use of cat. We're buffering the file here to avoid conditions # where we edit the file we're reading and face issues. done < <(cat "$file") done