Inquirer EntertainmentGrace Lee reveals pregnancy with ‘miracle baby’ at 43Daily MaverickWHAT’S COOKING: A steak salad with baby potatoes, broccoli, green beans, tomatoes and two dressingsPunchDSS arraigns man over fake Tinubu letter to US PresidentThe Jerusalem PostChannel 14 reporter verbally assaulted, harassed while covering voting in HaifaBollywood HungamaSaif Ali Khan and Vidya Balan reunite for Ritesh Batra’s Long Years Ago; Mélanie Laurent joins castESPNTransfer rumors, news: Real Madrid eye Liverpool's Alexis Mac AllisterInquirerKidapawan Diocese mourns desecration of parish churchUOLSignos: confira seu horóscopo desta terça (27)ZDF heuteAktuelle Pressemitteilungen des ZDFKhaosod EnglishIsraelis vote in a tense national election seen as a referendum on NetanyahuBBC NewsTruce begins in Ukraine city where residents ate weeds to surviveObservador DesportoAs notícias das 9h
The Daily Newsstand · Free, Always
Tuesday, October 27, 2026

Как я делаю защищенное файловое хранилище — 5: Безальтернативный SQLite

Translate

Часть 1: Рождение идеи
Часть 2: Ограничения и возможности
Часть 3: PostgreSQL против MongoDB
Часть 4: Пришествие gocryptfs
Часть 5: Безальтернативный SQLite (вы здесь)

Ссылка на репозиторий в конце статьи.

1. Когда в гости приходят гости, которых никто не приглашал

Первый раз я увидел скорпиона весной. Он выползал из моего дома на улицу. Из моего дома. На улицу.

Скорпион показался мне просто огромным. В тот момент я стоял у входа и эта встреча застала нас обоих врасплох.

Скорпион остановился на пороге, и мы молча смотрели друг на друга. Полагаю, он прикидывал, насколько я опасен. Я же прикидывал, достаточно ли хорошо обут.

А потом скорпион побежал. Обратно ко мне домой. Хотя я уже не был уверен в том, чей это дом.

Вы видели, как бежит скорпион, если чувствует опасность? Во-первых, очень быстро. Я не ожидал от него такой прыти. Во-вторых, он разгибает хвост горизонтально и использует его для балансировки. Почему-то в тот момент мне подумалось, что так бегают динозавры.

Я догнал его через пару метров. Сомневаться было некогда (простите). При контакте с подошвой скорпион громко хрустнул и бежать перестал. Но все равно пытался куда-то ползти. Такой живучести можно было только позавидовать. В любом случае, надеюсь, он не очень мучался.

Поскольку я не был уверен в том, насколько скорпионы социальные животные, то весь остаток дня искал его друзей. Я передвигал по дому все, что можно было передвинуть. Я заглядывал подо все, подо что можно было заглянуть. Но в тот раз больше никого не нашел.

Зато у меня мгновенно появилась привычка внимательно осматривать место, куда планирую присесть, прилечь или обуться. И еще я перестал оставлять открытой дверь на улицу.

2. И снова база данных

После перехода на gocryptfs снова возник вопрос: какую базу данных использовать? Как PostgreSQL, так и MySQL были предназначены для работы с локальной файловой системой. Их работа опирается на файловые блокировки, кэширование и синхронизацию данных с диском. При этом файловые системы, реализованные через FUSE, могут отличаться по поведению файловых операций от ext4 или XFS. Для большинства приложений это не выглядело серьезной проблемой, но в случае с MySQL или PostgreSQL все было гораздо сложнее.

Разумеется, я решил проверить все на практике. По очереди разместил данные PostgreSQL и MySQL внутри gocryptfs и попробовал запустить их в такой конфигурации.

Очень быстро стало понятно, что хорошей идеей это не было. Обе базы создавали совсем не ту нагрузку, при которой дополнительный слой FUSE остается незаметным: множество небольших чтений и записей, синхронизация данных с диском, работа с журналами и постоянные изменения одних и тех же файлов. Все эти операции теперь проходили через gocryptfs, а затем через FUSE к реальной файловой системе.

Но основные проблемы возникали при попытках размонтировать gocryptfs во время работы баз данных. В зависимости от способа размонтирования операция либо завершалась ошибкой, либо приводила к сбоям в работе баз. При этом самостоятельно проверять crash consistency и корректность восстановления после таких ситуаций было не той задачей, на которую мне хотелось бы тратить время. Некоторое время я всё же экспериментировал с настройками, но с каждой новой итерацией надежность всей этой конструкции стремительно падала в моих глазах.

В конечном счете я пришел к выводу, что даже если и получится собрать рабочую конфигурацию, доверия к ее надежности у меня не будет. И после нескольких неудачных экспериментов я отказался от этой затеи и стал искать вариант с более простой моделью хранения.

3. SQLite вырывается вперед

Единственной реалистичной альтернативой оказался SQLite. Он представляет собой обычный файл, не требует отдельного серверного процесса и официально допускает работу через FUSE. Но и здесь не обошлось без ограничений.

SQLite поддерживает несколько режимов журналирования. Во время изменения данных база сначала записывает служебную информацию в специальный журнал, чтобы в случае сбоя можно было откатить незавершенную транзакцию и восстановить согласованное состояние базы данных. Разные режимы отличаются тем, как именно ведется этот журнал, а также требованиями к файловой системе, производительностью и поведением при конкурентном доступе:

WAL (Write-Ahead Logging) — самый распространенный. Изменения сначала записываются в отдельный журнал, а затем переносятся в основной файл базы данных. Это повышает производительность и позволяет читать базу данных, пока другая транзакция выполняет запись.

DELETE — классический режим. Перед изменением базы исходные данные записываются в отдельный журнал, который удаляется после завершения транзакции.

TRUNCATE — работает почти так же, как DELETE, но после завершения транзакции журнал не удаляется, а очищается до нулевого размера. Это позволяет избежать части операций с файловой системой.

PERSIST — файл журнала также сохраняется на диске, но вместо удаления или очистки его заголовок помечается как недействительный. При следующих транзакциях файл используется повторно.

MEMORY — журнал хранится только в оперативной памяти. Это сокращает количество операций с диском, но лишает базу возможности восстановиться после аварийного завершения процесса.

OFF — журналирование полностью отключается. Это уменьшает накладные расходы при записи, но база перестает быть защищена от повреждения в случае сбоя посреди транзакции.

Разумеется, первым делом я попробовал режим WAL. Первый же тест закончился печально. Стоило принудительно размонтировать gocryptfs при открытом соединении с базой данных, как после повторного монтирования SQLite уже не мог открыть базу.

Причина была довольно простой. В режиме WAL база данных состоит уже не из одного файла, а как минимум из трех: основного файла базы, журнала -wal и файла общей памяти -shm. Во время работы SQLite постоянно поддерживает согласованность между ними. Если файловая система исчезает прямо во время работы, этот процесс прерывается, а согласованность файлов нарушается. После повторного монтирования база данных оказывается поврежденной.

В конечном счете из всех режимов меня заинтересовали только классические rollback-режимы: DELETE, TRUNCATE и PERSIST. Они полностью совместимы с FUSE, а различия между ними сводятся главным образом к тому, что происходит с файлом журнала после завершения транзакции. Поскольку в моем случае это не имело принципиального значения, я остановился на стандартном режиме DELETE. И на практике этот вариант оказался полностью стабильным.

Из этого следовало еще одно важное решение. Несмотря на то что приложение должно использовать асинхронный доступ к базе данных, запускать несколько Uvicorn-воркеров не имеет смысла. SQLite все равно допускает только одну транзакцию записи одновременно, поэтому дополнительные процессы не увеличат производительность, а лишь усложнят синхронизацию между ними. В итоге приложение должно будет работать с одним воркером, а асинхронность позволит эффективно обслуживать множество одновременно ожидающих запросов.

4. Установка и первый запуск

Теперь можно было наконец довести до конца запуск приложения. Получилась довольно простая цепочка, которая запускается одной командой:

Makefile     >  Dockerfile    >  entrypoint  >  application

Установка и     Создание         Подготовка     Исключительно
служебные       переносимого     контейнера     бизнес-логика
команды         окружения        к запуску      приложения

Всё начинается с Makefile. По большому счету это лишь удобная оболочка над командами сборки и запуска, которая избавляет от необходимости каждый раз вручную вводить длинные команды docker. Для установки достаточно выполнить make install.

Дальше в работу вступает Dockerfile. Он собирает полностью готовый образ. В него устанавливаются Python, fuse3, gocryptfs и стандартные зависимости самого приложения. В результате получается переносимое окружение, которое можно запустить на любой машине с установленным Docker.

После старта контейнера управление сначала получает небольшой entrypoint-скрипт. Он подготавливает окружение к работе: при необходимости создает конфигурационный файл, загружает параметры окружения и передает управление самому приложению.

Последним шагом запускается Uvicorn. К этому моменту уже готовы конфигурация и окружение, поэтому FastAPI остается только начать принимать запросы.

Если приложение запускается впервые, после старта остается выполнить еще один дополнительный шаг — задать мастер-пароль от хранилища. После этого приложение генерирует случайную gocryptfs passphrase, шифрует ее этим мастер-паролем и сохраняет в томе с секретами. В открытом виде на диск не попадают ни gocryptfs passphrase, ни мастер-пароль.

5. Базовые правила работы хранилища

Теперь предстояло разобраться с тем, как именно приложение должно хранить пользовательские данные. Общая архитектура уже была определена: само приложение работает внутри Docker-контейнера, а всё ценное находится снаружи, в подключаемых томах.

Таких томов два. Первый предназначен для зашифрованных данных, второй — для секретов, необходимых для их расшифровки. Разделение было принципиальным: если данные и ключи окажутся в одном месте, вся идея защиты потеряет значительную часть смысла.

Первый том я назвал cipherdir. Это обычный каталог на хостовой файловой системе, в котором хранятся зашифрованные файлы и каталоги. Их содержимое и имена зашифрованы, но сама структура каталогов остается видимой (это особенность работы самого gocryptfs). При этом ни само приложение, ни пользователи не должны обращаться к содержимому cipherdir напрямую.

Для работы с файлами предназначен другой каталог — mountpoint. Он находится внутри контейнера и служит точкой монтирования gocryptfs. Когда хранилище открыто, в этом каталоге появляется расшифрованное представление содержимого cipherdir. Для приложения это все выглядит как обычная файловая система с обычными данными.

Таким образом, одни и те же данные существуют в двух представлениях: зашифрованном — во внешнем томе, и расшифрованном — только внутри контейнера и только во время монтирования. При любых файловых операциях gocryptfs на лету выполняет шифрование и расшифровку полностью прозрачно для самого приложения.

Второй внешний том предназначен для секретов. В нем должен храниться зашифрованный пароль gocryptfs, который, в свою очередь, защищен мастер-паролем владельца. Такой том можно разместить на съемном носителе или подключать из другого источника. Главное, чтобы приложение могло определить его отсутствие.

Теперь получалось установить несколько простых правил.

Во-первых, контейнер не должен содержать никаких незаменимых данных. Должна быть возможность его остановить, удалить и создать заново. Если оба внешних тома сохранились и доступен мастер-пароль, состояние приложения можно полностью восстановить.

Во-вторых, расшифрованные данные должны быть доступны только при открытом хранилище. Пока gocryptfs не смонтирован, приложение не должно обращаться к пользовательским файлам и базе данных. После размонтирования доступ к ним должен прекращаться.

В-третьих, секреты должны оставаться отделенными от данных. Наличие одного только cipherdir не должно позволять открыть хранилище. Для этого потребуются и секреты из второго тома, и мастер-пароль владельца.

И наконец, приложение должно самостоятельно управлять жизненным циклом файловой системы. Открытие хранилища означает проверку мастер-пароля и монтирование gocryptfs. Закрытие — прекращение операций с данными и размонтирование. Причем это должно работать не только по команде владельца, но и при возникновении обстоятельств, требующих немедленного закрытия хранилища.

В результате получалась довольно простая конструкция. Контейнер можно пересоздать. Том с зашифрованными данными можно скопировать или перенести. Том с секретами можно физически отключить. А расшифрованное представление файлов доступно только тогда, когда приложение открывает хранилище.

Оставалось научить приложение соблюдать эти правила. Особенно последнее.

6. Конец пятой части

Дальше предстояло подумать над жизненным циклом хранилища: монтированием, размонтированием и контролем секретов. И самое главное — пришло время Dead Man's Switch.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.