Skip to main content

Command Palette

Search for a command to run...

dpkg/apt 패키지 잠그기

Updated
4 min read

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 에 등록하고 삭제하려 시도했을 때:

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