raforg / daemon

turns other processes into daemons
http://libslack.org/daemon/
GNU General Public License v2.0
14 stars 2 forks source link

Consider reversing the order of `daemon_become_user` and `prog_err_file`. #7

Open shamefulCake1 opened 2 months ago

shamefulCake1 commented 2 months ago

I would like to have daemon running as root supervise a non-privileged process (running as nobody).

I am running "daemon" like this:

daemon --name=myprogram \
       --respawn --attempts=1 --delay=10 \
       --user=nobody \
       --stderr=/var/log/daemon/myprogram.process.log \
       --errlog=/var/log/daemon/myprogram.daemon-supervisor.log \
       --command='/home/user/bin/myprogram'

This plan has the following security niceties:

  1. nobody by default has no shell, and I don't have to give one to it.
  2. nobody by default cannot write into either /var/log/daemon/myprogram.process.log or /var/log/daemon/myprogram.daemon-supervisor.log, it is "really unpivileged".
  3. daemon when run as root cannot be killed or otherwise damaged by someone who intrudes into the system and hijacks access to nobody via, say, a vulnerability in myprogram.

Since daemon run as root can do [2], and by default has a shell, it can open writing to the desired file before dropping privileges. However, since version "0.3 (20000902)", daemon is dropping root privileges very early, so doesn't allow the setup of [1], [2], [3].

Would it be possible to add an option to drop privileges later?

raforg commented 1 month ago

My initial reaction to this is to recommend using syslog destinations for --stderr and --errlog. But I can see the point of not wanting to require syslog (e.g. when you don't have root access to configure where the log messages end up, but you do have root access here so that's less of an issue).

Please be aware that daemon when used with --user does not run as root. When invoked by root, it starts as root but changes user and groups to that of the intended user before starting the client process. So daemon and the client process both run as the intended user. So [3] won't happen, even if the user is changed after opening files for --stderr and --errlog. But of course, [3] is possible if the client process changes the user. For example, if you had --command='sudo -u nobody cmd arg...', that would satisfy [2] as well. And [1] is just always true regardless of what you do with daemon. Note that the user's login shell isn't relevant with daemon. If the client command contains shell metacharacters, then sh is used, and if it doesn't, then the command is executed without sh.

So I'm pretty sure what you want can already be achieved just by using sudo.

I'll need to think about opening stdout/stderr destination files earlier. It's intentional that the user/groups are changed as early as possible, so that only the intended user's permissions are relevant when reading config files and opening output files. It might take me awhile to think about any bad consequences of changing it.

But in the meantime, please use sudo. Actually, it's probably better to use su instead, since there's no need to modify the sudo config.

Since what you want can be achieved by using sudo/su in conjunction with daemon, I'm unlikely to conclude that this change is needed, but I will think about it and possibly do it. But it won't be soon (think months).

shamefulCake1 commented 1 month ago

When invoked by root, it starts as root but changes user and groups to that of the intended user before starting the client process. So daemon and the client process both run as the intended user. So [3] won't happen, even if the user is changed after opening files

I would say that this is not very practical. If I could (wanted) to use su, I would have wrapped the whole daemon invocation in su -c 'daemon ...' nobody. Having a --user= option would, however, provide the capability unavailable to one using su in a straightforward way, that is having those log files (and only them) open for writing where it normally would not be able to.

So I'm pretty sure what you want can already be achieved just by using sudo.

Most of daemon's functionality can be achieved by judicious use of bash loops, sleep, su, logger, and similar shell utilities (maybe even setting proc title if/when the code for the proctitle builtin gets into Bash. But a single daemon invocation is just so much more straightforward and less error-prone.

I mean, up to you, of course, but to me it seems more practical to use all the powers allowed to root to achieve precisely the required running conditions, and then drop the privileges.

raforg commented 1 month ago

If I could (wanted) to use su, I would have wrapped the whole daemon invocation in su -c 'daemon ...' nobody

Since you have root privileges, you can use su, either the way you describe, or the way I describe. But the way you describe won't achieve your stated goal of being able to write to your log files. As for not wanting to use su, I don't understand the objection to using su when it contributes elegantly to solving your problem.

Most of daemon's functionality can be achieved by judicious use of bash loops, sleep, su, logger, and similar shell utilities

Those tools cannot provide the fundamental feature of daemon which is to enter a daemon context for programs that are unable to do it for themselves. nohup(1) and setsid(1) would get close (but without the ability to restart or lock a pidfile to ensure a singleton daemon). But yes, I'm sure that could be done if you prefer.

I mean, up to you, of course, but to me it seems more practical to use all the powers allowed to root ...

Maybe I'm too paranoid about security (that's unlikely to change), and want to minimize the risk of any potential security flaws in daemon itself being exploited. From that point of view, the sooner the user is changed the better. And I don't agree that a few extra characters in the client command is a problem, especially since daemon's -u/--user option would no longer be needed. It might actually be the same number of characters:

daemon -u nobody --command='cmd arg...'
daemon --command='su nobody cmd arg...'

and then drop the privileges.

Unless I misunderstood your original statement, you were originally under the impression that daemon itself did not drop privileges at all, and only ran the client process as the specified user. You indicated that you wanted/expected daemon to remain as a root process so that it couldn't be influenced by the client process in the event that the client process was exploited in some way. I proposed a way of acheiving what I understood to be your stated goals. Even if daemon did drop privileges after opening output files, rather than before, it still wouldn't achieve all of your stated goals.

If you really don't like my proposal, I'd recommend looking at other daemonizing programs. There are many many many on github. They don't have as many features as this daemon program, but one of them might do what you want the way you want. It would be better than writing a shell script and trying to achieve a similar effect with many processes. But then again, writing that shell script might be a lot of fun!

shamefulCake1 commented 1 month ago

Unless I misunderstood your original statement, you were originally under the impression that daemon itself did not drop privileges at all, and only ran the client process as the specified user. You indicated that you wanted/expected daemon to remain as a root process so that it couldn't be influenced by the client process in the event that the client process was exploited in some way. I proposed a way of acheiving what I understood to be your stated goals. Even if daemon did drop privileges after opening output files, rather than before, it still wouldn't achieve all of your stated goals.

I expected a rootful daemon to be supervising a nobody-run child, with certain system-level interactions being done with root privileges. This can be possibly implemented in different ways, I think.

My idea was that the parent (supervising) daemon would read the config file, spawn the child, the child daemon (while still being run as "root"), would redirect its own stderr/stdout to files, then drop privileges and exec the actual child program.

There should be, as far as I understand, another way of achieving the same result:

The parent daemon opens the child's stderr/stdout files, then creates two pipes for interacting with the child, then spawns the child daemon, which drops the privileges (as early as possible), redirects its own stdout/stderr to the pipes, and exec's the child.

I do think that making daemon read user's config files while being launched as root is confusing.

Maybe I'm too paranoid about security (that's unlikely to change), and want to minimize the risk of any potential security flaws in daemon itself being exploited. From that point of view, the sooner the user is changed the better. And I don't agree that a few extra characters in the client command is a problem

I appreciate the assiduousness with respect to security, it is certainly a very prudent attitude, but I don't think that a system-level tool can be made secure by such a crude measure as switching the user early. I would very much more prefer trusting the daemon's logic and code quality, rather than brushing those properties under the carpet by changing the user. (This is why I tried compiling it with UBSAN in the first place.) And for the same reason I am hesitant to study other daemonising software or, God forgive, systemd. Smaller codebase, longer history, smaller scope -- those are the markers of software security and stability. Changing the user later allows more fine-grained control of privileges.

It might actually be the same number of characters:

It is not the number of characters which is in play, but the hidden complexity which it incurs. su means launching another subprocess, another command-line interpreters, the necessity to care about shell behavioural differences, care about shell syntax (and messing up with bash syntax is to easy).

Again, I completely understand your approach, and you are the boss of daemon, but I suspect that it might be safer for me to fork daemon and exchange the two function calls (which I actually already did), rather than keep jumping from a daemoniser to a daemoniser, each of which will not be perfect and might require forking anyway.