Skip to content

Conversation

@t41372
Copy link

@t41372 t41372 commented Sep 28, 2025

Summary

This PR introduces optional, compile-time support for Encryption at Rest for SQLite databases by integrating SQLCipher. This provides a robust security enhancement for self-hosted instances, protecting data from unauthorized filesystem access.

This feature is opt-in and must be enabled at build time using the memos_sqlcipher build tag.

Documentation, including enabling encryption and migrating existing data has been added to the README. More documentation is written at the end of this PR description. I'm still a bit unsure about the doc site.

Note that:

  • this is not end-to-end encryption. The data are encrypted at rest. The application (memos) would still feel like its dealing with plaintext data.
  • this feature is optional.
  • Losing the key means the data is permanently lost.
  • this pr is just for SQLIte, not for Postgre or other stuff.

Relevant Issues:

Motivation

Currently, Memos' SQLite database is stored as a plaintext db file on disk. If a server's filesystem is compromised or the database file is inadvertently exposed (like a backup leak or cloud providers), all user data is at risk. By implementing full-database encryption, we can mitigate this risk, ensuring that the data remains secure even if the database file is stolen.

Implementation Details

  • SQLCipher Integration: Replaced the pure-Go modernc.org/sqlite driver with the CGo-based github.com/mattn/go-sqlite3 to enable linking against the SQLCipher C library.
  • Opt-in Build: The feature is isolated using the memos_sqlcipher build tag. Users who don't need encryption can continue to build Memos without CGo dependencies.
  • Docker Support: The Dockerfile has been updated to support building an encryption-ready image. It correctly handles build-time (sqlcipher-dev) and runtime (sqlcipher-libs) dependencies.
  • Configuration: The encryption key is supplied at startup via the --sqlite-encryption-key command-line flag or the MEMOS_SQLITE_ENCRYPTION_KEY environment variable.

How to Use (Docker Example)

  1. Build the Image:

    docker build \
      --build-arg CGO_ENABLED=1 \
      --build-arg MEMOS_BUILD_TAGS="memos_sqlcipher libsqlite3 sqlite_omit_load_extension" \
      -t memos-sqlcipher \
      -f scripts/Dockerfile .
  2. Run the Container:

    docker run \
      -d --name memos \
      -p 5230:5230 \
      -v ~/.memos/:/var/opt/memos \
      -e MEMOS_SQLITE_ENCRYPTION_KEY="your-super-secret-key" \
      memos-sqlcipher

Migrating an Existing Database

Users can encrypt an existing SQLite database without installing additional tools on the host by leveraging the SQLCipher-enabled Docker image.

Migration Steps:

  1. Backup the plaintext memos_prod.db (and any -wal/-shm files).
  2. Build the SQLCipher image as shown above (memos-sqlcipher).
  3. Run the conversion inside a one-off container:
    docker run --rm \
      -v ~/.memos:/data \
      memos-sqlcipher \
      sh -c "cd /data && sqlcipher memos_prod.db <<'EOS'\nATTACH DATABASE 'memos_encrypted.db' AS encrypted KEY 'your-secret-key';\nSELECT sqlcipher_export('encrypted');\nDETACH DATABASE encrypted;\nEOS"
  4. Swap files (mv memos_prod.db memos_prod.db.plaintext && mv memos_encrypted.db memos_prod.db && rm -f memos_prod.db-wal memos_prod.db-shm).
  5. Restart Memos with the SQLCipher build and supply the passphrase via MEMOS_SQLITE_ENCRYPTION_KEY (or the CLI flag).

Important Notes

  • This feature provides Encryption at Rest, not End-to-End Encryption (E2E). The server process still holds the key and accesses data in plaintext.
  • The user is solely responsible for their encryption key. Losing the key means the data is permanently lost.

Notes for Reviewers

  • Optional CGO dependency: The build remains pure-Go by default (modernc.org/sqlite). CGO is only required when consumers compile with -tags memos_sqlcipher. Reviewer should verify this does not affect existing release pipelines.
  • New runtime dependency when encrypted: The SQLCipher build relies on libsqlcipher/libsqlite3. Docker packaging now installs these libraries and symlinks them appropriately; downstream packagers will need similar tooling if they ship their own binaries.
  • go-sqlite3 inclusion: github.com/mattn/go-sqlite3 is now in go.mod. Although only pulled in under the SQLCipher tag, this increases the module graph; ensure this is acceptable for distribution.
  • Migration guide: README now documents both Docker-based and manual migration paths. Please confirm the documentation aligns with maintainers' expectations.
  • Failure modes: When a key is provided to a non-SQLCipher build, startup now fails fast with a clear error. Ensure this behaviour is desired versus silently ignoring the key.

More Documentation

Database Encryption with SQLCipher

Memos now supports transparent, full-database encryption for SQLite using the integrated SQLCipher library. This feature provides robust Encryption at Rest for your data.

What It Is and What It Protects

Encryption at Rest means your database file (e.g., memos_prod.db) is fully encrypted on the disk. Without the correct key, its contents are unreadable.

This protects you from:

  • Physical theft of the server or its hard drives.
  • Unauthorized access to the filesystem (e.g., via another compromised service).
  • Data leaks from backups.

[!IMPORTANT]
This is NOT End-to-End Encryption (E2E). The encryption and decryption operations occur on the Memos server. The Memos application process holds the key in memory and works with your data in plaintext. It does not protect data from an attacker who has already compromised the running Memos application.


How to Enable Encryption

Enabling encryption is a two-step process: building Memos with the correct build tag and providing the encryption key at runtime.

Step 1: Build Memos with SQLCipher Support

You must compile a version of Memos that includes SQLCipher support using the memos_sqlcipher build tag.

Using Docker (Recommended)

This is the simplest and recommended method. From the project root, build a SQLCipher-enabled Docker image:

docker build \
  --build-arg CGO_ENABLED=1 \
  --build-arg MEMOS_BUILD_TAGS="memos_sqlcipher libsqlite3 sqlite_omit_load_extension" \
  -t memos-sqlcipher \
  -f scripts/Dockerfile .

Building from Source

  1. Ensure you have the SQLCipher development libraries installed. The package is often named sqlcipher-dev or libsqlcipher-devel.
    # On Debian/Ubuntu
    sudo apt-get update && sudo apt-get install -y sqlcipher-dev
  2. Build the binary using the required tags:
    CGO_ENABLED=1 go build -tags="memos_sqlcipher libsqlite3 sqlite_omit_load_extension" -o memos ./bin/memos/main.go

Step 2: Provide the Encryption Key at Runtime

You must provide your encryption key when starting the Memos server.

Using Docker

Pass the key as an environment variable in your docker run command:

docker run \
  -d --name memos \
  -p 5230:5230 \
  -v ~/.memos/:/var/opt/memos \
  -e MEMOS_SQLITE_ENCRYPTION_KEY="your-super-secret-key" \
  memos-sqlcipher

Using the Binary

Pass the key as a command-line flag or as an environment variable:

# As a command-line flag
./memos --driver sqlite --sqlite-encryption-key "your-super-secret-key"

# Or as an environment variable
export MEMOS_SQLITE_ENCRYPTION_KEY="your-super-secret-key"
./memos --driver sqlite

Key Management and Security

[!WARNING]
Your Key, Your Responsibility!

  • Losing the key means losing your data. If you forget or lose your encryption key, your database will be permanently unrecoverable. There is no backdoor or "forgot password" option.
  • Back up your key in a secure location, such as a password manager.
  • Secure your key in production. Avoid passing keys directly in shell scripts. Use a secure method like Docker Secrets, Kubernetes Secrets, or a dedicated secret management system to inject environment variables.
  • Backups of your database file will be encrypted. This is good, but they are useless without the key. Ensure your key is backed up separately and securely.

Migrating an Existing Database

To encrypt an existing, unencrypted Memos database, you must use the sqlcipher command-line tool.

  1. Stop your Memos service.

  2. Back up your original database file! This is critical.

    cp memos_prod.db memos_prod.db.bak
  3. Install the sqlcipher command-line tool.

    # On Debian/Ubuntu
    sudo apt-get install -y sqlcipher
  4. Perform the encryption process:

    # 1. Open your unencrypted database with the sqlcipher tool
    sqlcipher memos_prod.db
    
    # 2. (Inside the sqlcipher prompt) Attach a new encrypted database and set your key
    > ATTACH DATABASE 'memos_prod_encrypted.db' AS encrypted KEY 'your-super-secret-key';
    
    # 3. (Inside the sqlcipher prompt) Export the data from the old DB to the new one
    > SELECT sqlcipher_export('encrypted');
    
    # 4. (Inside the sqlcipher prompt) Detach the new database and exit
    > DETACH DATABASE encrypted;
    > .exit
  5. Replace the old database file with the new encrypted one:

    mv memos_prod_encrypted.db memos_prod.db
  6. You can now start your Memos service with the encryption key, and it will read the newly encrypted database.


FAQ

  • Can I change the encryption key?
    Yes. Open your encrypted database with the sqlcipher command-line tool and run PRAGMA rekey = 'new-secret-key';.

  • What is the performance impact?
    SQLCipher is highly optimized. The performance impact is generally minimal and should be negligible for most operations. The main overhead occurs when the database connection is first opened and during heavy write operations.

This index.html probably modified after I ran pnpm command. Wonder why it's not in .gitignore...

Anyway, it's not related to this pr, so I guess I will revert it to make sure the pr is clean.
@t41372 t41372 requested a review from boojack as a code owner September 28, 2025 10:20
@lincolnthalles
Copy link
Contributor

Encryption would be great as it's a recurring request. But the memo content itself should be encrypted, so it would cover all database drivers at once and wouldn't require any special builds.

Using an instance encryption key, similar to what your patch does, may be a quick way to get this jump-started, though on many deployments, the key would still exist on the server, so it can auto-start.

Note that the project is currently not very friendly to handling edge cases and feature creeping, favoring simplicity, so it's best to make a proposal and get an okay before proceeding with big changes, unless it really suits you and you see no problem in maintaining a separate fork.

A simpler way to obtain encryption at rest is to encrypt the partition where the database is.

Doing user data encryption properly is challenging, especially at advanced levels, like implementing zero-knowledge storage (like meganz does), with each user having their own encryption key for their memos, so even the instance admin couldn't eavesdrop. It would also spin off other needs, like private attachments. You would really need to get the main maintainers on board for something at this level, as it's a big undertaking.

Btw, libsqlite3 is slower as it relies on CGO, which also has the downside of breaking Go's native cross compilation capabilities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants