Utilizing macOS local APFS snapshots for Restic backups

Hey all :slight_smile:

I’ve been in front of this problem for several months now and can’t seem to find a proper solution.

Plan:
I want to use macOS local APFS snapshots as the source of restic backups.
So whenever restic is scheduled to run a backup command, my idea is to make a local APFS snapshot, mount it and then point restic at this read only mounted APFS snapshot.
All this logic is put in a bash script and being scheduled with launchd.

Problem:
In order to automatically mount a local APFS snapshots with this script, I have to add bash to the list of FDA (Full Disk Access) applications in the macOS settings pane.
I have tried all the tricks I could think of but couldn’t find a way other than the above.

From a security perspective I absolutely don’t want to give bash full disk access to my machine.
That’s just not going to happen.

So I guess this isn’t restic specific I guess, but around it heh
Here is a little screenshot showing how I set it up and reproduced it:


If someone wants to play around with it, here is a minimal setup which reproduced the problem:

demo.sh:

#!/bin/bash
set -x
mkdir -p /tmp/restic/backup

local_apfs_snapshot_full="$(tmutil localsnapshot /)"
local_apfs_snapshot=$(echo "$local_apfs_snapshot_full" | grep 'Created local' | awk '{ print $6 }')

mount_apfs -s com.apple.TimeMachine."$local_apfs_snapshot".local /Users/ "/tmp/restic/backup"

# Cleanup
umount "/tmp/restic/backup"
rm -rf /tmp/restic/backup

exit 0

com.moritzdietz.restic-backup.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN
http://www.apple.com/DTDs/PropertyList-1.0.dtd>
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.moritzdietz.restic-backup</string>
    <key>Program</key>
    <string>/Users/moritzdietz/Desktop/demo.sh</string>
    <key>RunAtLoad</key>
    <true/>
    <key>StartInterval</key>
    <integer>900</integer>
    <key>StandardOutPath</key>
    <string>/Users/moritzdietz/Library/Logs/com.moritzdietz.restic-backup.out.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/moritzdietz/Library/Logs/com.moritzdietz.restic-backup.error.log</string>
</dict>
</plist>

Then you need to go into the FDA settings and add and remove /bin/bash to see the result.

1 Like

FWIW, I have a bash script that runs restic which I wrap as a macOS .app using GitHub - sveinbjornt/Platypus: Create native Mac applications from command line scripts. . I then install that .app in ~/Applications, give that .app FDA in the System Settings, and it can then happily read the files it needs to.

I only ever backed up ~ or stuff thereunder, in case that matters. But what I can say is that without giving that .app FDA it will produce a lot of read errors during backup runs, and having given it FDA it’s happy. I’ve tried other methods too through the years but this one always works for me.

1 Like

Ha! Thanks. Will definitely give that a try and report back if that works.

The thing is, I know of another backup software which has allowed access to Apples private APFS APIs and I am super jealous that they are gatekeeping it from others.

Making an .app bundle using the software you recommended worked to mount the local APFS snapshot! Thank you :slight_smile:

Now I need to see if using launchd to schedule this, works as well. But a first test by simply starting the .app with the bash script which invokes restic, tmutil and mount_apfs commands worked perfectly.

FWIW I use /usr/bin/open -ga $HOME/Applications/Backup.app to run the .app from crontab.

Hey all :slight_smile: Another year went by and another major macOS release comes around the corner bringing all kinds of “joy”. So this post seems rather unrelated to Restic itself, but as I’m trying to figure out how to use a macOS feature to do macOS backups via Restic, I thought I post it here in the case others have ventured out this route as well.

Apple must have changed things around FDA (Full Disk Access) and TCC (Transparency, Consent, and Control) with apps.

For some reason some things broke. So if someone is using a similar setup with:

  • Shell script bundled in an app like Platypus
  • The Shell script creates a local APFS snapshot AND mounts it for use with Restic

I would love to hear if and how you got that working.

Creating a snapshot is not an issue, but mounting it results in an error. The script from the beginning of the post is able to reproduce the problem.

What I’m confused about is this: If I add my Terminal app, in this case Alacritty, to the list in FDA and execute the Bash script manually, everything works as expected. Below is the output of the script running and the commands that are executed.

But not when I wrap the script in an app via Platypus:

I see some errors in the console.app regarding this and I have a hunch that this is because of some security flag. Not sure if it’s SIP but I bet it’s related.

I’ll keep digging in the meantime

Ha! I think I solved it. Man this is somewhat involved if you don’t know your ways around and have some understanding of the security features of macOS.

The solution was: Signing the Platypus created app bundle with a self signed Code Signing certificate. :tada:

Here is what I did:

  1. In the Keychain Access app using the Certificate Assistant, I created a local Code Signing certificate which is trusted only by my computer as it is the CA issuing the certificate
  2. Created an app bundle via Platypus which contains the Bash script and put it in /Applications/
  3. Signed the app bundle like this:
    codesign -s 'Restic B2 (test) - Moritz Dietz' '/Applications/Restic B2 - 3.app'
    Check the man page for codesign for more info
    3.1 For good measure I checked that it worked:
codesign --verify --verbose '/Applications/Restic B2 - 3.app'
/Applications/Restic B2 - 3.app: valid on disk
/Applications/Restic B2 - 3.app: satisfies its Designated Requirement
  1. Removed an earlier entry of the app bundle from the FDA list in the Systems Settings app (they renamed it with macOS Ventura :roll_eyes:)
  2. Re-added the new app bundle to the FDA list and switched the toggle to on

Then started the app and here’s a screenshot of it working :tada:

4 Likes

In a setup like this (with snapshots), your restic backup doesn’t contain /Users but /tmp/restic/backup, right?

If so, restoring from such a backup cannot be done ‘in place’. Or is there something I am missing? E.g. can I tell during backup/restore to change every path? E.g. on backup make sure that /tmp/restic/backup becomes /Users so that a restore can be in place?

restic does not support inplace restore anyway…

It might be added only in the next release Roadmap for restic 0.17 to 0.19

Funny. I am curious what exactly happened when I used restore -t / to restore /opt/local from a snapshot last week, as in my experience it did just that (but maybe it worked only partly and I was just lucky)

Are you sure your permission problem isn’t due to the fact that / has been changed to /System/Volumes/Data several macOS releases ago (and thus your /Users should be /System/Volumes/Data/Users)?

That’s what I had to do to make it work—I didn’t have to code-sign.

Yeah I’m pretty sure, because of what I see in the console log (see the post above the solution).

Additionally I want to point out that I don’t backup /Users/ on the live system, I backup a mounted APFS Snapshot of / which is mounted at /tmp/restic/backup/ and makes my home directory accessible at /tmp/restic/backup/Users/.