3 votos

Desmontar el volumen al cerrar la sesión del usuario

Tengo varios volúmenes APFS encriptados, algunos de los cuales tienen sus frases de paso almacenadas sólo en los llaveros de usuarios específicos para que puedan desbloquearse y montarse automáticamente para esos usuarios.

Sin embargo, cuando el usuario cierra la sesión, el volumen sigue montado. Aunque esto no es estrictamente un problema (ya que establezco puntos de montaje dentro de la carpeta de inicio del usuario, y tengo habilitada la propiedad del volumen), preferiría que el volumen se desmontara y se bloqueara de nuevo automáticamente.

Este comportamiento difiere en comparación con el montaje de una imagen de disco cifrada, que se monta como un usuario específico (al ver la salida de mount ) y que luego (normalmente*) se desmonta cuando el usuario cierra la sesión.

¿Es posible replicar el mismo comportamiento con los volúmenes APFS, es decir, montar automáticamente para el usuario con la frase de contraseña en su llavero cuando se conecta, y luego desmontar (y bloquear) automáticamente cuando se desconecta?

*Digo normalmente, ya que MacOS Catalina parece tener errores relacionados con la limpieza de los procesos de usuario cuando un usuario cierra la sesión, lo que hace que muchos procesos sigan funcionando para los usuarios que han cerrado la sesión. Esto incluye actualmente a disk-image-helper, por lo que ya no se desmonta automáticamente como lo hacía en Mojave. Estoy feliz de aceptar cualquier respuesta que replique el comportamiento de montaje/desmontaje de imágenes de disco con la advertencia de que esto actualmente no siempre funciona para Catalina, sobre la base de que debería funcionar si Apple alguna vez corrige estos errores.

Actualización : He intentado hacer lo siguiente en un script disparado por launchd, pero la señal de kill de launchd no parece llegar:

#!/bin/bash
VOLUME=12345678-9012-3456-7890-123456789012
MOUNT_POINT=/Users/haravikk/Desktop/Foo

[ ! -e "${MOUNT_POINT}" ] && { mkdir "${MOUNT_POINT}" || exit 1; }

if echo -e "$(security find-generic-password -wa "${VOLUME}" | sed 's/../\\x&/g')" | diskutil apfs unlockVolume "${VOLUME}" -stdinpassphrase; then
    cleanup() {
        echo 'Unmounting'

        attempts=5
        while [[ ${attempts} -gt 0 ]]; do
            diskutil apfs lockVolume "${VOLUME}" && break
            [[ -n "${MOUNT_POINT}" ]] && umount "${MOUNT_POINT}" && break
            attempts=$((${attempts} - 1))
            sleep 5
        done
        if [[ ${attempts} = 0 ]]; then
            if ! diskutil unmount force "${VOLUME}"; then
                if [[ -z "${MOUNT_POINT}" ]] || ! umount -f "${MOUNT_POINT}"; then
                    echo 'All attempts to unmount failed' >&2
                fi
            fi
        fi
    }
    trap 'cleanup' SIGINT SIGHUP SIGTERM EXIT
    while true; do
        sleep 86400 &
        wait $!
    done
fi

La idea era que cuando el usuario se desconectara, launchd deben enviar a sus procesos una señal de muerte ( SIGINT ) que activará la trampa del script y le permitirá desmontar el volumen. Pero esto nunca parece suceder; la trampa no se dispara en absoluto.

Si alguien está interesado en utilizar los fundamentos de este script, tenga en cuenta que necesitará tener una entrada en su llavero para el volumen (puede hacerlo montando con la Utilidad de Discos y eligiendo guardar cuando se le pida la contraseña), y debe asegurarse de que security tiene permiso para acceder a ella.

0voto

Ted Wrigley Puntos 101

Puede intentar utilizar un gancho de cierre de sesión . Los hooks de inicio y cierre de sesión están obsoletos, pero creo que siguen funcionando.

Montar el volumen no debería ser un problema; un LaunchAgent de usuario se encargaría de ello sin problemas. El problema radica en tratar de desmontar el volumen al cerrar la sesión. ¿Ha considerado escribir un LaunchDaemon del sistema que periódicamente sondea los volúmenes APFS abiertos y desmonta aquellos que no tienen un usuario asociado? Los volúmenes deberían bloquearse automáticamente por la seguridad del sistema cuando se desmontan, así que no creo que tengas que hacer un esfuerzo especial para ello, y parece que estás pensando más en la limpieza que en otra cosa. Si pones el demonio en (digamos) un temporizador de 30 segundos, no debería consumir demasiados recursos, y los volúmenes sólo persistirán durante una media de 15 segundos después de cerrar la sesión.

0voto

Haravikk Puntos 332

Después de mucha experimentación he llegado a una solución con scripts, con un script que puede operar en dos modos:

La primera es como un demonio, que suele ejecutarse como root (como demonio de lanzamiento), que escucha en un determinado socket los comandos que identifican el volumen que se quiere montar (debe estar desmontado), seguido de otro para confirmar que lo montó (demostrando que puede hacerlo), y un tercero para luego desmontarlo, forzar su desmontaje o borrar la petición. El comportamiento es un poco simplista, pero debería establecer razonablemente que un cliente tiene la capacidad de montar el volumen, y por lo tanto se le permite solicitar que luego sea desmontado, usando una simple credencial aleatoria.

Cuando no se ejecuta en modo demonio, el script toma un identificador de volumen (cualquier cosa soportada por diskutil apfs unlockVolume (preferiblemente UUIDs) e intenta desbloquear y montar el volumen. Es necesario tener la contraseña del volumen en el llavero del usuario que ejecuta el script, y se le pedirá que permita security para acceder a ella. El scriptnormalmente intenta desmontar un volumen por sí mismo, sin embargo he establecido que la mayoría de las veces esto no funcionará, ya que el arbitraje del disco suele descargarse antes de que el scriptintente hacerlo (es decir diskutil unmount y umount ambos fallan), por lo que si quieres usar este script con un agente de lanzamiento que se desmonte al cerrar la sesión, necesitas tener un demonio ejecutándose en el mismo sistema y establecer el --socket para que coincida.

MonteAPFS

Esperemos que esto sea bastante claro en cómo se supone que se utiliza, ya que incluye ejemplos y las opciones están documentadas. No está pensado para nadie que no tenga conocimientos de uso de la Terminal y de scripts de shell (ZSH específicamente), ya que puede que tengas que personalizarlo para hacer exactamente lo que quieres.

#!/bin/zsh
{

# Examples:
#   Standalone: ./MountAPFS 12345678-9012-3456-7890-12345678901234
#      (mount): ./MountAPFS --create ~/Library/Volumes/Foo 12345678-9012-3456-7890-12345678901234
#
#   Daemon:     ./MountAPFS --daemon --socket 61616
#   Client:     ./MountAPFS --socket 61616 12345678-9012-3456-7890-12345678901234

while [ $# -gt 0 ]; do
    case "$1" in
        # Set a directory that needs to be created (usually the volume's mount point when a custom mount point is specified in /etc/fstab)
        ('--create'|'--create-dir'|'--create-directory')
            CREATE_DIRECTORY="$2"; shift
            case "${CREATE_DIRECTORY:0:1}" in
                ('/')   ;;
                ('~')   CREATE_DIRECTORY="${HOME}${CREATE_DIRECTORY:1}"   ;;
                (*)     CREATE_DIRECTORY="${BASE_DIRECTORY}/${CREATE_DIRECTORY}"  ;;
            esac
        ;;
        # Runs this script in daemon mount (do not mount any volumes, instead handle the unmount of registered volumes on behalf of other tasks).
        ('--daemon') DAEMON=1 ;;
        # The socket to listen on/connect to when working in/with a daemon script
        ('--socket') SOCKET="$2"; WAIT=1; shift ;;
        # The amount of time to wait for the volume to become available before giving up. This option can be used if there may be a race condition between this and another task before the volume becomes available
        ('--timeout') TIMEOUT="$2"; shift ;;
        # Do not end once the volume is mounted, instead wait for a termination signal and attempt to unmount it
        ('--wait') WAIT=1 ;;
        # Enable verbose output; this will output volume identifiers and tokens for tracing, but will only output the last four characters of tokens to prevent abuse (full tokens are 32 characters in length)
        ('-v'|'--verbose') VERBOSITY=$(($(echo "0${VERBOSITY}" | sed 's/[^0-9]*//g') + 1)) ;;
        # Explicit end of arguments
        ('--') shift; break ;;
        (--*) echo "Unknown option: $1" >&2; exit 2 ;;
        # Implicit end of arguments (first volume)
        (*) break ;;
    esac
    shift
done

VERBOSITY=$(echo "0${VERBOSITY}" | sed 's/[^0-9]*//g')

if [[ -n "${SOCKET}" ]]; then
    [[ "${SOCKET}" = "$(echo "${SOCKET}" | sed 's/[^0-9]*//g')" ]] || { echo 'Invalid socket:' "${SOCKET}" >&2; exit 2; }
    [[ "${SOCKET}" -gt 0 ]] || { echo 'Invalid socket:' "${SOCKET}" >&2; exit 2; }
fi

if [ "${DAEMON}" = 1 ]; then
    [[ -n "${SOCKET}" ]] || { echo 'Daemon mode requires a socket' >&2; exit 2; }

    # Open netcat on the specified socket
    coproc nc -kl localhost "${SOCKET}" || { echo 'Unable to open socket' >&2; exit 2; }
    trap 'coproc :' EXIT SIGHUP SIGINT SIGTERM

    [[ ${VERBOSITY} -gt 0 ]] && echo 'APFS daemon listening on socket:' "${SOCKET}"

    declare -A requested=()
    declare -A mounted=()
    while IFS='', read -rd '' line; do
        cmd="${line:0:5}"
        value="${line:5}"
        case "${cmd}" in
            # Indicates intention to mount a current unmounted volume (given in value).
            # Returns a token that must be used in future commands
            ('mount')
                if mount=$(diskutil info "${value}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" = 'No' ]]; then
                    token=$(echo "${value}$(head -c 512 </dev/urandom)" | md5)
                    requested[${token}]=${value}
                    printf '%s%s\0' 'mount' "${token}" >&p

                    [[ ${VERBOSITY} -gt 0 ]] && echo 'Accepted mount request for:' "${value} assigned token ending with:" "${token: -4}"
                else
                    printf '%s%s\0' 'error' 'Volume not found, or is already mounted' >&p
                    [[ ${VERBOSITY} -gt 0 ]] && echo 'Volume not found or already mounted:' "${value}" >&2
                fi
            ;;
            # Indicates that the previously registered volume is now mounted. Volume is identified using the unique token returned by the mount command. Now that the volume has been mounted, it can be unmounted using the unmnt or funmt command.
            # Returns the volume that was tested
            ('mnted')
                volume=${requested[$value]}
                if [ -n "${volume}" ]; then
                    if mount=$(diskutil info "${volume}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" != 'No' ]]; then
                        mounted[${value}]=${volume}
                        unset "requested[${token}]"
                        printf '%s%s\0' 'mnted' "${volume}" >&p

                        [[ ${VERBOSITY} -gt 0 ]] && echo 'Confirmed mounting of:' "${volume} using token ending with:" "${value: -4}"
                    else
                        printf '%s%s\0' 'error' 'Volume not found, or is not mounted' >&p
                        [[ ${VERBOSITY} -gt 0 ]] && echo 'Volume not found or not mounted:' "${volume}" >&2
                    fi
                else
                    printf '%s%s\0' 'error' 'Unknown token: use the mount command first' >&p
                    [[ ${VERBOSITY} -gt 0 ]] && echo "Received ${cmd} command out of sequence or invalid token ending with: ${token: -4}" >&2
                fi
            ;;
            # Requests that a previously mounted volume to be unmounted. Volume is identified using the unique token used in the mnted command.
            # The funmt command will attempt to forcibly unmount the volume, and should only be used if the unmnt command previously failed.
            # Returns the volume that was unmounted
            ('unmnt'|'funmt')
                volume=${mounted[$value]}
                if [ -n "${volume}" ]; then
                    if mount=$(diskutil info "${volume}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" != 'No' ]]; then
                        [ "${cmd}" = 'funmt' ] && force='force ' || force=''

                        if error=$(diskutil unmount ${force}"${volume}" 2>&1); then
                            unset "mounted[${token}]"
                            printf '%s%s\0' "${cmd}" "${volume}" >&p

                            [[ ${VERBOSITY} -gt 0 ]] && echo 'Unmounted volume:' "${volume} using token ending with:" "${token: -4}"
                        else
                            printf '%s%s\0' 'error' "Unable to unmount ${volume}: ${error}" >&p
                            [[ ${VERBOSITY} -gt 0 ]] && echo 'Unable to mount:' "${volume}: ${error}" >&2
                        fi
                    else
                        printf '%s%s\0' 'error' 'Volume not found, or is not mounted' >&p
                        [[ ${VERBOSITY} -gt 0 ]] && echo 'Volume not found:' "${volume}" >&2
                    fi
                else
                    printf '%s%s\0' 'error' 'Unknown token: use the mnted command first' >&p
                    [[ ${VERBOSITY} -gt 0 ]] && echo "Received ${cmd} command out of sequence: expected mnted" >&2
                fi
            ;;
            # Clear a token that is no longer needed
            ('clear')
                unset "requested[${value}]"
                unset "mounted[${value}]"

                printf '%s%s\0' 'clear' "${value}" >&p

                [[ ${VERBOSITY} -gt 0 ]] && echo 'Cleared token ending with:' "${value: -4}"
            ;;
            # Unknown command
            (*)
                printf '%s%s\0' 'error' "Unknown command: ${cmd}" >&p
                [[ ${VERBOSITY} -gt 0 ]] && echo 'Received unknown command:' "${cmd}" >&2
            ;;
        esac
    done <&p

    coproc :
    [[ ${VERBOSITY} -gt 0 ]] && echo 'Terminating.'
else
    [[ -z "${BASE_DIRECTORY}" ]] && BASE_DIRECTORY="${HOME}/Library/Vaults/"
    [[ -d "${BASE_DIRECTORY}" && -w "${BASE_DIRECTORY}" ]] || { echo 'Missing or unwritable base directory:' "${BASE_DIRECTORY}" >&2; exit 1; }

    [[ $# -lt 1 ]] && { echo 'Missing volume' >&2; exit 1; }
    VOLUME="$1"

    # If a timeout was given, wait until the volume is ready
    TIMEOUT=$(echo "${TIMEOUT}" | sed 's/[^0-9]*//g')
    if [[ -n "${TIMEOUT}" ]]; then
        while [[ "${TIMEOUT}" -gt 0 ]]; do
            diskutil info "${VOLUME}" 2>&1 >/dev/null && break
            TIMEOUT=$((${TIMEOUT} - 5))
            sleep 5
        done
    fi

    # Make sure the volume is available to be unlocked
    error=$(diskutil info "${VOLUME}" 2>&1) || { echo 'Volume not found:' "${VOLUME}:" "${error}" >&2; exit 3; }

    # If a mount point was given, try to create a directory (otherwise volume won't mount over it)
    if [[ -n "${CREATE_DIRECTORY}" ]] && [[ ! -d "${CREATE_DIRECTORY}" ]]; then
        error=$(mkdir -m 700 "${CREATE_DIRECTORY}") || { echo 'Unable to create mount point:' "${CREATE_DIRECTORY}:" "${error}" >&2; exit 4; }
    fi

    # If a socket was given, register our intention to mount the volume
    token=
    if [[ "${WAIT}" = 1 && -n "${SOCKET}" ]]; then
        socket_cmd() { local cmd="$1"; local value="$2"
            coproc nc localhost "${SOCKET}" || { echo 'Unable to connect to socket' >&2; return 1; }

            local response=
            printf '%s%s\0' "${cmd}" "${value}" >&p
            read -rd '' response <&p

            case "${response:0:5}" in
                ("${cmd}")
                    printf '%s' "${response:5}"
                    coproc :
                    return 0
                ;;
                ('error')
                    echo "socket_cmd() error: ${response:5}" >&2
                    coproc :
                    return 2
                ;;
                (*)
                    echo 'Unknown/unsupported response:' "${response}" >&2
                    coproc :
                    return 3
                ;;
            esac
        }
        token=$(socket_cmd 'mount' "${VOLUME}") || SOCKET=
    fi

    if error=$(echo -e "$(security find-generic-password -wa "${VOLUME}" | sed 's/../\\x&/g')" | diskutil apfs unlockVolume "${VOLUME}" -stdinpassphrase) || error2=$(diskutil mount "${VOLUME}"); then
        if [[ "${WAIT}" = 1 ]]; then
            # Confirm mounting of volume to socket (if registered)
            [[ -n "${token}" ]] && { volume_confirm=$(socket_cmd "mnted" "${token}") || token=; }

            printf '%s' 'Awaiting signal... '

            # Trap and wait until task is ended, then lock the volume
            cleanup_run=0
            cleanup() {
                [[ ${cleanup_run} = 0 ]] || return 0
                cleanup_run=1

                echo 'received.'
                printf '%s' 'Unmounting... '

                attempts=5
                while [[ ${attempts} -gt 0 ]]; do
                    diskutil apfs lockVolume "${VOLUME}" >/dev/null && echo 'done.' && break
                    [[ -n "${CREATE_DIRECTORY}" ]] && umount "${CREATE_DIRECTORY}" && echo 'done.' && break
                    [[ -n "${token}" ]] && volume_confirm=$(socket_cmd 'unmnt' "${token}") && token= && echo 'done.' && break
                    attempts=$((${attempts} - 1))
                    sleep 5
                done
                if [[ ${attempts} = 0 ]]; then
                    if diskutil unmount force "${VOLUME}" >/dev/null; then
                        echo 'forced.'
                    else
                        if [[ -z "${CREATE_DIRECTORY}" ]] || ! umount -f "${CREATE_DIRECTORY}"; then
                            if [[ -z "${token}" ]] || ! volume_confirm=$(socket_cmd 'funmt' "${token}"); then
                                echo 'failed.'
                                echo 'All attempts to unmount failed' >&2
                            else
                                token=
                                echo 'forced.'
                            fi
                        else
                            echo 'forced.'
                        fi
                    fi
                fi
                [[ -n "${token}" ]] && socket_cmd 'clear' "${token}"

                # Clear all background tasks
                coproc :
                [[ -n "${${(v)jobstates##*:*:}%=*}" ]] && kill ${${(v)jobstates##*:*:}%=*}
            }
            trap 'cleanup' SIGINT SIGHUP SIGTERM EXIT
            while true; do
                sleep 86400 &
                wait $!
            done
        fi
    else
        echo 'Unable to mount volume:' "${error}" "${error2}" >&2
        [[ -n "${token}" ]] && socket_cmd 'clear' "${token}"
    fi
fi

}

AppleAyuda.com

AppleAyuda es una comunidad de usuarios de los productos de Apple en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X