dpkg/apt 패키지 잠그기
dpkg 는 상당히 안정적이고 잘 만들어진 패키지 관리자라고 생각하지만, 특정 조건에서의 제어 능력이 아쉬울 때가 있다. 예를 들어 특정 패키지 설치/업데이트/제거를 차단하고자 할때 (우발적 변경을 방지) 이를 제어할 수 없다.
dpkg 에는 패키지를 hold 하는 기능이 있지만 이것으로는 부족하다. 따라서 dpkg wrapper 스크립트를 만든 후 이를 dpkg-divert 로 dpkg 처럼 작동하게 만들었다.
/usr/sbin/dpkg-wrapper.sh:
#!/bin/bash
set -e
# Block based on pattern
if [[ ! -z "$(echo "$@" | grep \\--remove)" ]] || # Removal
[[ ! -z "$(echo "$@" | grep \\-r)" ]] || # Removal
[[ ! -z "$(echo "$@" | grep \\--purge)" ]] || # Purge
[[ ! -z "$(echo "$@" | grep \\--install)" ]] || # Install
[[ ! -z "$(echo "$@" | grep \\-i)" ]] || # Install
[[ ! -z "$(echo "$@" | grep \\--reinstall)" ]] || # Reinstall
[[ ! -z "$(echo "$@" | grep \\--upgrade)" ]] || # Upgrade
[[ ! -z "$(echo "$@" | grep \\-U)" ]] || # Upgrade
[[ ! -z "$(echo "$@" | grep \\--unpack)" ]] || # apt install
[[ ! -z "$(echo "$@" | grep \\--auto-deconfigure)" ]]; then # apt install
# Trigger python script to handle the logic
python3 /usr/sbin/dpkgCmdParser.py "$@"
# Check exit code
if [ $? -ne 0 ]; then
echo "Operation not permitted by system policy."
exit 1
fi
fi
exec /usr/bin/dpkg.distrib "$@"
위 코드는 dpkg 가 받는 매개변수를 읽어들인 후 패키지 변조를 하는 매개변수가 들어왔을 때 dpkgCmdParser.py 로 모든 매개변수를 넘긴 후, 파서에서 반환 코드 0을 확인한다. 만약 파서가 0을 반환하지 않았을 경우 장치 정책에 따라 변조 불가능한 패키지가 안에 들어있음을 의미하기에 실제 dpkg (dpkg.distrib) 을 실행하지 않는다.
/usr/sbin/dpkgCmdParser.py
# Read registry
# Not-removables: HKEY_LOCAL_MACHINE/SOFTWARE/Policies/ProtectedPackages/<package>.dword.rv = 1
# Install blacklist: HKEY_LOCAL_MACHINE/SOFTWARE/Policies/BlacklistedPackages/<package>.dword.rv = 1
import sys
import subprocess
import os
from oscore import libreg as reg
def _chk_registry_protect_mode(package_name: str) -> bool:
key_path = f"SOFTWARE/Policies/ProtectedPackages/{package_name}"
protected = reg.read(key_path, 0)
if protected == 1:
return False # Cannot remove protected package
return True
def _chk_registry_install_mode(package_name: str) -> bool:
key_path = f"SOFTWARE/Policies/BlacklistedPackages/{package_name}"
protected = reg.read(key_path, 0)
if protected == 1:
return False # Cannot install blacklisted package
return _chk_registry_protect_mode(package_name)
def _local_id(raw_args: list[str], install_mode: bool) -> int:
# Parse package names
# This usually looks like this:
# --status-fd 21 --no-triggers --unpack --auto-deconfigure libyyjson0 fastfetch
# Parsing will directly read the package names from the arguments.
for element in raw_args:
# Package name
# Note: This does not guarantee the element is a valid package name.
if not element.startswith("-"):
package_name = element
# Note: package name contains : architecture suffix
if ":" in package_name:
package_name = package_name.split(":", 1)[0]
if install_mode:
if not _chk_registry_install_mode(package_name):
return 1 # Block installation
else:
if not _chk_registry_protect_mode(package_name):
return 1 # Block removal
# Not package name
else:
pass
return 0 # Allow operation
def _file_path(raw_args: list[str], install_mode: bool) -> int:
# Parse package names
# This usually looks like this:
# --status-fd 21 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/libyyjson0_0.12.0+ds-1_amd64.deb /var/cache/apt/archives/fastfetch_2.57.1+dfsg-1_amd64.deb
# or if alot, then looks like this:
# --status-fd 21 --no-triggers --unpack --auto-deconfigure --recursive <path>
# Parsing will execute 'dpkg-deb -f <filename.deb> Package' in subprocess to fetch the package name.
if "--recursive" in raw_args:
recurse_index = raw_args.index("--recursive")
if recurse_index + 1 < len(raw_args):
dir_path = raw_args[recurse_index + 1]
if os.path.isdir(dir_path):
for root, _, files in os.walk(dir_path):
for file in files:
if file.endswith(".deb"):
deb_path = os.path.join(root, file)
raw_args.append(deb_path)
for element in raw_args:
# Deb file
if element.endswith(".deb") and os.path.isfile(element):
try:
result = subprocess.run(
["/usr/bin/dpkg-deb", "-f", element, "Package"],
capture_output=True,
text=True,
check=True
)
package_name = result.stdout.strip()
if package_name:
if install_mode:
if not _chk_registry_install_mode(package_name):
return 1 # Block installation
else:
if not _chk_registry_protect_mode(package_name):
return 1 # Block removal
except subprocess.CalledProcessError as e:
print(f"Error fetching package name from {element}: {e}", file=sys.stderr)
return 1
# Not deb file
else:
pass
return 0 # Allow operation
def main() -> int:
args: list[str] = sys.argv[1:]
using_local_id = any(flag in args for flag in [
"--remove",
"-r",
"--purge",
"--uninstall",
"--configure",
"--upgrade",
"-U"])
is_removal = any(flag in args for flag in ["--remove", "--purge", "-r", "--uninstall"])
return _local_id(args, not is_removal) if using_local_id else _file_path(args, not is_removal)
if __name__ == "__main__":
import sys
sys.exit(main())
위 코드는 필자의 AquariusOS 의 레지스트리 시스템에서 소프트웨어 정책을 읽어와 체크하는 파이썬 코드이다. 해당 코드는 dpkg 가 변조하려는 큐에서 Blacklist (삭제만 가능) 혹은 Protected (변조 불가능) 상태를 확인한 후 통과 여부를 반환하는 코드이다.
위 두 코드를 모두 /usr/sbin/에 넣고, 다음 명령어를 sudo 권한으로 실행하여 적용한다:
dpkg-divert --divert /usr/bin/dpkg.distrib --rename /usr/bin/dpkg
ln -sf /usr/sbin/dpkg-wrapper.sh /usr/bin/dpkg
chmod +x /usr/sbin/dpkg-wrapper.sh
chmod +x /usr/bin/dpkg
fastfetch 명령어를 Protected 에 등록하고 삭제하려 시도했을 때:

이후 등록을 해제하고 다시 삭제하려 하였을 때:

