This isn't a guide about locking down homebrew so that it can't touch the rest of your system security-wise.
This guide doesn't fix the inherent security issues of a package management system that will literally yell at you if you try to do something about "huh, maybe it's not great my executables are writeable by my account without requiring authorization first".
But it absolutely is a guide about shoving it into its own little corner so that you can take it or leave it as you see fit, instead of just letting the project do what it likes like completely taking over permissions and ownership of a directory that might be in use by other software on your Mac and stomping all over their contents.
By following this guide you will:
- Never have to run
sudo
to forcefully change permissions of some directory to be owned by your account - Pick and choose exactly what binaries from homebrew you want to have exposed to your environment
- Make it possible to effectively "unhook" the installed homebrew environment on-demand
- You could even have multiple separate installs side-by-side, if you wanted
I create a lot of my own tools - and other times I'll have tools built by others that I just like to keep somewhere non-standard to avoid some app or something I install that relies on an opensource component that decides to ship those components in "their standard location" and ends up clobbering /usr/local
or other paths.
So to that end, I created a .local
folder in my home directory. The structure you'd find inside it is the same as you'd see in /usr
or /usr/local
, with a bin
, lib
, share
, etc. I have this path prepended onto the front of my $PATH
for my shell:
export PATH="~/.local/bin:$PATH"
The .
at the beginning of .local
ensures that if I'm looking in my home folder in the Finder, it won't clutter the view unless I reveal hidden files. You could choose any name you like if you want to do this. And because it's at the front of my $PATH
, anything I link into .local/bin
takes precedence - I know I'll be running a specific instance of a binary named foo
if I put or link something named foo
into it.
If you want a little extra security here, feel free to adjust the path permissions so that you can't write or add new things to this path without elevating. (But it won't be much, mind - homebrew will still be doing its thing as our unauthenticated user account in the end, as you'll see)
homebrew (grudgingly?) supports being installed in a non-standard folder. They just don't suggest it to you up front. It's tucked away in the detailed installation page under "Alternative Installs":
https://docs.brew.sh/Installation#untar-anywhere
What's the downside of doing this?
- A lot of precompiled packages will now require compiling, because it's not the standard path so the linked library paths in the prebuilt binaries won't work. As a concrete example: Installing the
ghc
package after freshly setting up homebrew as listed above required compilation - total time was 8 minutes and 7 seconds including all the dependencies. - Some (poorly built) packages potentially won't work because they make assumptions about path. In practice, I haven't run into this yet.
What's the upside of doing this?
- It's not camped out in
/usr/local
or/opt
- You're not blindly directly executing a script you just curl'd down
- If you've got a managed IT environment that thinks massively distributing or managing a default path homebrew install across engineering laptops is a good idea, by doing this you won't be fighting with their install or having to run
brew doctor
to repair permissions flips - You never have to enter your password or use
sudo
to set it up - (And more we'll get into shortly here)
So I cd
into my ~/.local
and then follow the main instruction they have there:
mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
That's it really. You've got homebrew installed now. Since it was installed in your home directory - no need to change any permissions. No sudo
required.
But you still have to patch in brew
itself to your shell session, since you've got it tucked away right now in ~/.local/homebrew/bin/brew
, which so far you haven't added to your $PATH
.
homebrew, as part of some of the other things it does that I don't like, if you symlink it into a bin
-like path that you've included in your $PATH
, when you execute it it will "helpfully" analyze the path it was called from and use that as a target destination for installing symlinks to all the binaries anything you install generates.
But we don't want that. We put baby in the corner and it needs to stay there.
So the trick is adding this to your shell environment: alias brew=~/.local/homebrew/bin/brew
This makes it so that brew
is available to your shell environment as a callable command, but what it's calling is the fully qualified path that brew
already lives in, as a subdirectory under ~/.local/homebrew
The net effect of this is: everything you install or update with brew stays in this folder
All of the binaries only get linked into ~/.local/homebrew/bin
Want to completely nuke this homebrew install? Remove the directory - done!
Done installing something with brew
?
You can now use ln
to symlink exactly what you please into ~/.local/bin
. So if the act of installing a single package in homebrew results in 5+ additional packages being installed, all with their own binaries, when it's finished installing everything and the kitchen sink you can make sure that only the exact single thing that you wanted in the first place is allowed to be seen outside of that directory (instead of finding out later that something had a side effect of installing a brew
copy of python you never wanted, etc).
This is why I said not to adjust your $PATH
to include homebrew/bin
itself to make brew
work. It would work - but it would also make every single thing you installed exposed as callable in your shell environment. We don't want that.
Worried about other projects that use headers or libraries installed by brew
?
Most of the ones I've seen are smart and use $(brew --prefix)
to automatically figure out the correct platform independent path your libraries are at. Easily 95% or more of what I see out there is doing this. There are some things you'll run into out there though where that might not be the case, but you can fix that almost always by using something like:
CFLAGS="-I$(brew --prefix)/include" LDFLAGS="-L$(brew --prefix)/lib"
It's easy. Just get rid of the alias. alias brew=
Now its existence isn't discoverable. All of your built packages are off in their ~/.local/homebrew
jail, but because it's on a non-standard path nothing is able to discover and link to the results - and because brew
itself can't be found, it can't be queried for library/linker paths.
There's the danger here still of them finding or tripping across the binaries you've linked into ~/.local/bin
, sure. But if you think you might want to take advantage of the ability to hide brew
products during a build, you could always keep that in a separate brewbin
path that you add or remove from your $PATH
.
Now that you understand the basics of how this works, there's lots of options and flexibility. Maybe try to fix some of the core security issues in homebrew and move its path and managing it to a dedicated service account. Do what you like - go wild!
But most importantly: don't let brew
win