Security -- auto-upgrades and config ownership

I just realized that I might have a significant security issue.

One of the things that I like about NixOS, from a system security perspective, is that I can enable auto-upgrades and enjoy automatic security updates with minimal fear about it borking my system.

But I just realized that my flake-based system is using a git repo that is owned by my user (not root), symlinked to /etc/nixos just for convenience.

This theoretically means that any attacker that breaches my user has a really easy escalation path – modify the user-owned git repo, and wait for an automatic upgrade to run overnight.

Am I missing something here?

If that’s the case, what do you all do to mitigate this attack route? Does everyone just have root-owned configurations?

2 Likes

I have the autoupgrade service pointed directly to my flake hosted on GitHub. Not sure if this is exactly “more” secure than having a user owned config on device, but I keep my Github account pretty locked down.

I assume your user on your device has access to the ssh keys for pushing to GitHub though? I guess not necessarily, but that would be pretty inconvenient to not be able to change the config from your device.

I’m supposing that just chown -R root:me and chmod 0640 might be what I do. That way my user’s git credentials should still work, but have to elevate to modify.

Except git pull will be a problem in that case.

In my case, my ssh keys are stored encrypted within my password manager(1password) which locks when I lock my computer or times out. Need to enter my user password or use my u2f key to unlock. This password has its own integrated ssh agent that works with git and ssh.

I can modify my config locally, and I sign commits and push with my ssh key.

I also have a weekly github action that updates the lock file. Which is why I have my autoupgrade service pointed at the repo, since the local copy isn’t always the latest.

I’ve got a worktree of my repo at /nix/var/nix/nixos-config whose files are owned by root and a shell alias which resets the worktree’s branch to my repo’s current master as superuser.

That way I can freely experiment in my home checkout and once I’m happy and have it committed up to master, I “commit” that master to the system worktree and rebuild.

It’s a bit hacky and slightly tricky to set up but it works because the git reset operation does not create new files for existing branches.

1 Like

That makes sense, though it seems to require a few extra steps. (I also try to keep my bitwarden vault on as few machines as possible.)

I just have an --update-input nixpkgs-stable in my upgrade flags which seems to work.

This workflow actually sounds much more practical than other ideas I’ve had.

Basically I would have ~/git/nixos where I tinker and test builds, then /etc/nixos as a separate directory, alias syncConfig='sudo cp ~/git/nixos/* /etc/nixos/' and then run syncConfig manually every once in a while (which would require my superuser password).

That’d also work. I’d recommend rsync -a --delete though and ignore .git.

It surely depends on the case, but on personal machines the stuff owned by my user tend to be considered more valuable than the stuff owned by root. (valuable in terms of preventing access by a malicious actor)

Yes, the traditional user-based separation of priviledges is quite useless on a desktop machine.

xkcd 1200

9 Likes

True, except that root effectively “owns” (or at least could trivially own) all your user stuff too, and could trivially install software that would give access to stuff not even on that machine (keylogger → password manager, ssh key passphrases, etc.).

I agree that getting your user account breached is a problem, but I think it’s disingenuous to pretend that a bad actor getting root is not clearly an even bigger problem.

Which was my point in this post – I hadn’t considered how autoUpgrade and a user-owned config means that if someone gets my user account they would also have a paved path to root.

I meant it more like defending already at the level of that user’s account and considering breach of root basically the same severity. Still, on some servers it may be a significant distinction, even for “human accounts” (it certain holds for various pseudo-accounts meant for daemons).

Yes, I think they are probably similar in many cases, but I still think the direct path to install system-level software such as a keylogger means that the root access compromises all your user data on the machine and also facilitates compromising data that’s not on the machine / data that your user wouldn’t have access to because it’s not present at rest.

But I think we’re saying roughly the same thing.

Regardless, I think I’ll likely move to a system that doesn’t autoUpgrade from a user-writable configuration.

I just found a solution to this problem with additional requirements specific to my situation. I’ll try to describe it in case it helps others.

My problem was that I wanted to let servers autoUpgrade daily (flake-based) for security.
I didn’t want to upload my config publicly to github, which is why I couldn’t just reference it with --update-input nixpkgs.
I also wanted to use nix-copy-closure (through colmena) for deployment, which is why I couldn’t point the autoUpgrade servervice to the store copy of my configuration directory like this system.autoUpgrade.flake = toString ./., because this apperently does not make it part of the runtime closure (which makes sense, the string is already known at build time and the configuration directory does not have a /bin folder). I tried out different ways to solve this but ultimately settled for this as sufficiently clean:

system.autoUpgrade = {
  enable = true;
  flake = toString /run/current-system/configuration;
  flags = ["--update-input" "nixpkgs" "--no-write-lock-file"];
};
system.extraSystemBuilderCmds = ''
    # necessary for autoUpgrade service
    cp -rf ${./.} $out/configuration
  '';
};

I guess it would make sense to think about replacing the option system.copySystemConfiguration through something to the system.extraSystemBuilderCmds-part, since it is way more general and does not seem to have significant drawbacks

I don’t understand what you mean here. Mine is not public on github, but I am using update-input nixpkgs.

Okay, then never mind.
Mine would have to be since its the only way for the servers to receive the flake other than having it local (and therefore as part of the runtime closure).

Ah ok, I missed (but should have realized) that you were trying to avoid having the config locally. Make sense now.