Would it not be much less harmful if I could allow it to just read the password hashes instead?
No. Exposing hashes is not much better than exposing passwords, it allows unlimited offline attacks. Just running all daemons as root isn't the solution either of course, as already stated, only very small portions of code should ever run privileged and thus have access to the hashes.
In debian you would just add the daemons user to the shadow group.
Which means every daemon running as this user has full access to the hashes. Any RCE vulnerability in any of the daemons could be used to steal the whole password database and use it for an offline attack, cracking the passwords.
pam_unix(8) as it is typically implemented has a serious design flaw. As far as PAM is concerned, nothing is specified regarding privileges
needed to do some authentication, and it's safe to assume the expectation is not to need any. But this breaks with
pam_unix(8) because it just wraps the classic Unix authentication: hash a supplied password and compare the result with the stored hash (for which, of course, you must be able to read that hash).
LinuxPAM implemented a special case for authenticating as yourself by including a suid-root helper binary that's automatically invoked by
pam_unix(8). Authenticating as yourself is typically needed for screen lockers. The risk with a suid-root binary in the PAM stack is that a malicious user could invoke it directly instead of via PAM and thus bypass any other restrictions (like rate-limiting the authentication attempts). Therefore, it could be abused as an "oracle" to crack passwords using brute-force. Restricting the binary to only ever authenticate as yourself makes it much less useful for such abuse.
Screen lockers like e.g. xscreensaver started to rely on this behavior of LinuxPAM. Where they previously shipped their own suid-root helpers, these were dropped. As this didn't work with FreeBSD's
pam_unix(8) any more, I tried to implement the same thing here, but our PAM maintainer was strictly against including that, offering an alternative using
pam_exec(8). That's how I finally created
security/unix-selfauth-helper. It does the same thing LinuxPAM is doing, but must be explicitly configured. Good enough to "fix" screen lockers.
Still all of this is nothing but an ugly hack around the fundamental design issue with
pam_unix(8). Really solving it would require to design and implement an authentication
service that's used for Unix auth instead of directly accessing the hashes, this service could then enforce policies (like, see above, rate limiting) and NO other code would ever need to see the hashes. But that's not an easy thing to do.
There's already a secure authentication mechanism that requires neither accessing any secrets nor ever exchange secrets over the wire: kerberos. If you want a secure setup that "just works", setup some kerberos KDC and configure
pam_krb5(8).