Add MacOS sandboxing to isolate script
This commit is contained in:
1 parent
d579c12080
commit
fb747989d0
1 file changed
+141
-7
@@ -1,12 +1,57 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
project_dir=$( pwd -P ) || exit 1
|
# macOS does not provide GNU readlink -e. Keep this compatible with Bash 3.2.
|
||||||
home_dir=$( readlink -e -- "${HOME}" ) || exit 1
|
resolve_path() (
|
||||||
|
local path="${1}" links=0 directory
|
||||||
|
[[ "${path}" = /* ]] || path="${PWD}/${path}"
|
||||||
|
while :; do
|
||||||
|
if [[ -d "${path}" ]]; then
|
||||||
|
cd -P -- "${path}" && pwd -P
|
||||||
|
exit "${?}"
|
||||||
|
fi
|
||||||
|
directory="${path%/*}"
|
||||||
|
cd -P -- "${directory:-/}" || exit 1
|
||||||
|
path="${PWD%/}/${path##*/}"
|
||||||
|
if [[ ! -L "${path}" ]]; then
|
||||||
|
if [[ ! -e "${path}" ]]; then
|
||||||
|
echo "Cannot resolve ${1}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${path}"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
links=$(( ${links} + 1 ))
|
||||||
|
if [[ "${links}" -gt 40 ]]; then
|
||||||
|
echo "Too many symlinks in ${1}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
path=$( readlink "${path}" ) || exit 1
|
||||||
|
[[ "${path}" = /* ]] || path="${PWD}/${path}"
|
||||||
|
done
|
||||||
|
)
|
||||||
|
|
||||||
|
platform=$( uname -s ) || exit 1
|
||||||
|
case "${platform}" in
|
||||||
|
Linux)
|
||||||
if ! bubblewrap=$( command -v bwrap ); then
|
if ! bubblewrap=$( command -v bwrap ); then
|
||||||
echo "bubblewrap (bwrap) is required" >&2
|
echo "bubblewrap (bwrap) is required" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
;;
|
||||||
|
Darwin)
|
||||||
|
if ! sandbox_exec=$( command -v sandbox-exec ); then
|
||||||
|
echo "sandbox-exec is required on macOS" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported platform: ${platform}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
project_dir=$( pwd -P ) || exit 1
|
||||||
|
home_dir=$( resolve_path "${HOME}" ) || exit 1
|
||||||
agents=(
|
agents=(
|
||||||
$( command -v claude )
|
$( command -v claude )
|
||||||
$( command -v codex )
|
$( command -v codex )
|
||||||
@@ -39,7 +84,9 @@ declare -a rw_binds
|
|||||||
declare -a bwrap_args
|
declare -a bwrap_args
|
||||||
declare -a bind_sources
|
declare -a bind_sources
|
||||||
declare -a bind_destinations
|
declare -a bind_destinations
|
||||||
|
declare -a hidden_paths
|
||||||
|
|
||||||
|
if [[ "${platform}" = Linux ]]; then
|
||||||
ro_binds=(
|
ro_binds=(
|
||||||
/usr
|
/usr
|
||||||
/lib
|
/lib
|
||||||
@@ -52,6 +99,19 @@ ro_binds=(
|
|||||||
/etc/group
|
/etc/group
|
||||||
"${HOME}/sources"
|
"${HOME}/sources"
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
# System libraries, certificates, and Intel/Apple Silicon Homebrew installs.
|
||||||
|
# Do not allow all of /System: /System/Volumes/Data also exposes user files.
|
||||||
|
for readonly_path in /System/Library /System/Volumes/Preboot/Cryptexes /System/Cryptexes \
|
||||||
|
/usr /bin /sbin /opt/homebrew \
|
||||||
|
/Library/Apple /Library/Developer /Library/Frameworks /Library/Keychains \
|
||||||
|
/Library/Preferences /private/var/db/dyld /private/var/db/timezone \
|
||||||
|
/etc/resolv.conf /etc/hosts /etc/ssl /etc/passwd /etc/group "${HOME}/sources"; do
|
||||||
|
if [[ -e "${readonly_path}" ]]; then
|
||||||
|
ro_binds+=("${readonly_path}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
for readonly_path in "${HOME}/.gitconfig" "${HOME}/bin" "${HOME}/.local" "${HOME}/.atuin" "${XDG_RUNTIME_DIR}"; do
|
for readonly_path in "${HOME}/.gitconfig" "${HOME}/bin" "${HOME}/.local" "${HOME}/.atuin" "${XDG_RUNTIME_DIR}"; do
|
||||||
if [[ -e "${readonly_path}" ]]; then
|
if [[ -e "${readonly_path}" ]]; then
|
||||||
@@ -59,7 +119,7 @@ for readonly_path in "${HOME}/.gitconfig" "${HOME}/bin" "${HOME}/.local" "${HOME
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
for readwrite_path in "${HOME}/.claude" "${HOME}/.claude.json" "${HOME}/.local/share/claude" "${HOME}/.pi" "${project_dir}"; do
|
for readwrite_path in /tmp "${HOME}/.claude" "${HOME}/.claude.json" "${HOME}/.local/share/claude" "${HOME}/.pi" "${project_dir}"; do
|
||||||
if [[ -e "${readwrite_path}" ]]; then
|
if [[ -e "${readwrite_path}" ]]; then
|
||||||
rw_binds+=("${readwrite_path}")
|
rw_binds+=("${readwrite_path}")
|
||||||
fi
|
fi
|
||||||
@@ -67,13 +127,13 @@ done
|
|||||||
|
|
||||||
# Resolve mount sources, but keep the paths applications expect as destinations.
|
# Resolve mount sources, but keep the paths applications expect as destinations.
|
||||||
for readonly_path in "${ro_binds[@]}"; do
|
for readonly_path in "${ro_binds[@]}"; do
|
||||||
bind_source=$( readlink -e -- "${readonly_path}" ) || exit 1
|
bind_source=$( resolve_path "${readonly_path}" ) || exit 1
|
||||||
bwrap_args+=("--ro-bind" "${bind_source}" "${readonly_path}")
|
bwrap_args+=("--ro-bind" "${bind_source}" "${readonly_path}")
|
||||||
bind_sources+=("${bind_source}")
|
bind_sources+=("${bind_source}")
|
||||||
bind_destinations+=("${readonly_path}")
|
bind_destinations+=("${readonly_path}")
|
||||||
done
|
done
|
||||||
for readwrite_path in "${rw_binds[@]}"; do
|
for readwrite_path in "${rw_binds[@]}"; do
|
||||||
bind_source=$( readlink -e -- "${readwrite_path}" ) || exit 1
|
bind_source=$( resolve_path "${readwrite_path}" ) || exit 1
|
||||||
bwrap_args+=("--bind" "${bind_source}" "${readwrite_path}")
|
bwrap_args+=("--bind" "${bind_source}" "${readwrite_path}")
|
||||||
bind_sources+=("${bind_source}")
|
bind_sources+=("${bind_source}")
|
||||||
bind_destinations+=("${readwrite_path}")
|
bind_destinations+=("${readwrite_path}")
|
||||||
@@ -90,7 +150,8 @@ fi
|
|||||||
# Mask secrets after all directory mounts so later binds cannot expose them.
|
# Mask secrets after all directory mounts so later binds cannot expose them.
|
||||||
for hide_path in "${git_config}" "${git_worktree_config}" "${project_dir}/.env"; do
|
for hide_path in "${git_config}" "${git_worktree_config}" "${project_dir}/.env"; do
|
||||||
if [[ -e "${hide_path}" ]]; then
|
if [[ -e "${hide_path}" ]]; then
|
||||||
hide_path=$( readlink -e -- "${hide_path}" ) || exit 1
|
hide_path=$( resolve_path "${hide_path}" ) || exit 1
|
||||||
|
hidden_paths+=("${hide_path}")
|
||||||
bwrap_args+=("--ro-bind" /dev/null "${hide_path}")
|
bwrap_args+=("--ro-bind" /dev/null "${hide_path}")
|
||||||
# A symlinked mount can expose the same file at another destination.
|
# A symlinked mount can expose the same file at another destination.
|
||||||
for i in "${!bind_sources[@]}"; do
|
for i in "${!bind_sources[@]}"; do
|
||||||
@@ -104,9 +165,82 @@ for hide_path in "${git_config}" "${git_worktree_config}" "${project_dir}/.env";
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ "${platform}" = Darwin ]]; then
|
||||||
|
# Seatbelt restricts the host filesystem; it does not provide Linux namespaces.
|
||||||
|
# Keep path values out of the profile source by using sandbox-exec parameters.
|
||||||
|
declare -a sandbox_parameters
|
||||||
|
declare -a macos_write_paths
|
||||||
|
macos_profile='(version 1)
|
||||||
|
(deny default)
|
||||||
|
(allow process-exec process-fork)
|
||||||
|
(allow process-info* (target same-sandbox))
|
||||||
|
(allow signal (target same-sandbox))
|
||||||
|
(allow sysctl-read)
|
||||||
|
(allow network*)
|
||||||
|
(allow mach-lookup
|
||||||
|
(global-name "com.apple.system.logger")
|
||||||
|
(global-name "com.apple.logd")
|
||||||
|
(global-name "com.apple.system.notification_center")
|
||||||
|
(global-name "com.apple.system.opendirectoryd.libinfo")
|
||||||
|
(global-name "com.apple.system.opendirectoryd.membership")
|
||||||
|
(global-name "com.apple.SystemConfiguration.configd")
|
||||||
|
(global-name "com.apple.trustd.agent")
|
||||||
|
(global-name "com.apple.securityd.xpc"))
|
||||||
|
(allow file-read-metadata)
|
||||||
|
; The dynamic loader needs access to the root directory, not its contents recursively.
|
||||||
|
(allow file-read* (literal "/"))
|
||||||
|
(allow file-read* (literal "/dev/random") (literal "/dev/urandom") (literal "/dev/zero"))
|
||||||
|
(allow file-read* file-write* file-ioctl
|
||||||
|
(literal "/dev/null") (literal "/dev/tty") (literal "/dev/ptmx")
|
||||||
|
(subpath "/dev/fd") (regex #"^/dev/ttys[0-9]+$"))
|
||||||
|
(allow pseudo-tty)'
|
||||||
|
|
||||||
|
add_macos_rule() {
|
||||||
|
local action="${1}" operations="${2}" path="${3}" filter="${4:-literal}"
|
||||||
|
local parameter="PATH_${#sandbox_parameters[@]}"
|
||||||
|
if [[ -z "${4}" && -d "${path}" ]]; then
|
||||||
|
filter=subpath
|
||||||
|
fi
|
||||||
|
sandbox_parameters+=("-D" "${parameter}=${path}")
|
||||||
|
macos_profile+=$'\n'"(${action} ${operations} (${filter} (param \"${parameter}\")))"
|
||||||
|
}
|
||||||
|
|
||||||
|
for readonly_path in "${ro_binds[@]}"; do
|
||||||
|
readonly_path=$( resolve_path "${readonly_path}" ) || exit 1
|
||||||
|
add_macos_rule allow 'file-read*' "${readonly_path}"
|
||||||
|
done
|
||||||
|
for readwrite_path in "${rw_binds[@]}"; do
|
||||||
|
readwrite_path=$( resolve_path "${readwrite_path}" ) || exit 1
|
||||||
|
add_macos_rule allow 'file-read* file-write*' "${readwrite_path}"
|
||||||
|
macos_write_paths+=("${readwrite_path}")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Keep a per-command TMPDIR in addition to access to the shared system /tmp.
|
||||||
|
sandbox_tmp=$( mktemp -d "${TMPDIR:-/tmp}/isolate.XXXXXXXX" ) || exit 1
|
||||||
|
trap 'rm -rf -- "${sandbox_tmp}"' EXIT
|
||||||
|
sandbox_tmp=$( resolve_path "${sandbox_tmp}" ) || exit 1
|
||||||
|
add_macos_rule allow 'file-read* file-write*' "${sandbox_tmp}"
|
||||||
|
|
||||||
|
# Later Seatbelt rules win. Keep these denies after every file-access allow.
|
||||||
|
for hide_path in "${hidden_paths[@]}"; do
|
||||||
|
add_macos_rule deny 'file-read* file-write*' "${hide_path}"
|
||||||
|
# Do not let a writable parent (for example .git) be renamed to bypass the deny.
|
||||||
|
for readwrite_path in "${macos_write_paths[@]}"; do
|
||||||
|
parent_path="${hide_path%/*}"
|
||||||
|
while [[ "${parent_path}" = "${readwrite_path}/"* ]]; do
|
||||||
|
add_macos_rule deny 'file-write-unlink' "${parent_path}" literal
|
||||||
|
parent_path="${parent_path%/*}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
TMPDIR="${sandbox_tmp}/" "${sandbox_exec}" "${sandbox_parameters[@]}" \
|
||||||
|
-p "${macos_profile}" "${coder}" "${@}"
|
||||||
|
exit "${?}"
|
||||||
|
fi
|
||||||
|
|
||||||
"${bubblewrap}" \
|
"${bubblewrap}" \
|
||||||
"${bwrap_args[@]}" \
|
"${bwrap_args[@]}" \
|
||||||
--tmpfs /tmp \
|
|
||||||
--proc /proc \
|
--proc /proc \
|
||||||
--dev /dev \
|
--dev /dev \
|
||||||
--share-net \
|
--share-net \
|
||||||
|
|||||||
Reference in new issue
Block a user