[GH-ISSUE #1413] Allow secrets to be stored in files locally instead of plaintext #895

Open
opened 2026-03-02 11:53:34 +03:00 by kerem · 8 comments
Owner

Originally created by @debsidian on GitHub (May 15, 2025).
Original GitHub issue: https://github.com/karakeep-app/karakeep/issues/1413

Describe the feature you'd like

While creating a docker-compose for Karakeep there are a couple of environmental variables that I'm uncomfortable leaving in plaintext.

  • OAUTH_CLIENT_SECRET
  • NEXTAUTH_SECRET
  • OPENAI_API_KEY
  • MEILI_MASTER_KEY

It would be nice if we could reference a local file for sensitive information such as this.

Describe the benefits this would bring to existing Karakeep users

Benefits are security and privacy of sensitive data.

Can the goal of this request already be achieved via other means?

Not to my knowledge.

Have you searched for an existing open/closed issue?

  • I have searched for existing issues and none cover my fundamental request

Additional context

No response

Originally created by @debsidian on GitHub (May 15, 2025). Original GitHub issue: https://github.com/karakeep-app/karakeep/issues/1413 ### Describe the feature you'd like While creating a docker-compose for Karakeep there are a couple of environmental variables that I'm uncomfortable leaving in plaintext. - `OAUTH_CLIENT_SECRET` - `NEXTAUTH_SECRET` - `OPENAI_API_KEY` - `MEILI_MASTER_KEY` It would be nice if we could reference a local file for sensitive information such as this. ### Describe the benefits this would bring to existing Karakeep users Benefits are security and privacy of sensitive data. ### Can the goal of this request already be achieved via other means? Not to my knowledge. ### Have you searched for an existing open/closed issue? - [x] I have searched for existing issues and none cover my fundamental request ### Additional context _No response_
Author
Owner

@Eragos commented on GitHub (May 17, 2025):

I'm always a security focused guy. But I don't get your point :-/

If you follow the basic security rules (secure passwords, SSL encryption, separate networks, latest updates/security fixes, you name it...). You shouldn't have a problem. Security comes from the weakest point of the whole chain. And a plain text file in a secured context is IMHO not the problem.

Some things you can read:

I invite you to further discuss this topic in the discussion section...

jmy2ct

Best Michael

<!-- gh-comment-id:2888461354 --> @Eragos commented on GitHub (May 17, 2025): I'm always a security focused guy. But I don't get your point :-/ If you follow the basic security rules (secure passwords, SSL encryption, separate networks, latest updates/security fixes, you name it...). You shouldn't have a problem. Security comes from the weakest point of the whole chain. And a plain text file in a secured context is IMHO not the problem. Some things you can read: - https://openid.net/developers/how-connect-works/ - https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety - https://www.meilisearch.com/docs/learn/security/basic_security I invite you to further discuss this topic in the discussion section... _jmy2ct_ Best Michael
Author
Owner

@debsidian commented on GitHub (May 17, 2025):

Maybe I'm miscommunicating here? I'm advocating for basic security practices. The ones that are baked into Docker's own documentation:

A secret is any piece of data, such as a password, certificate, or API key, that shouldn’t be transmitted over a network or stored unencrypted in a Dockerfile or in your application’s source code.

Docker Compose provides a way for you to use secrets without having to use environment variables to store information. If you’re injecting passwords and API keys as environment variables, you risk unintentional information exposure. Services can only access secrets when explicitly granted by a secrets attribute within the services top-level element.

Environment variables are often available to all processes, and it can be difficult to track access. They can also be printed in logs when debugging errors without your knowledge. Using secrets mitigates these risks.

Source

No third party links necessary. Docker tells you what best practices are. I'm just asking that they be implemented here. It's okay if the answer is 'no', but I figured it couldn't hurt to ask.

Edit: Just to be clear, the ask is simply to mount/link to a file within the docker-compose instead of putting the api key as a plaintext environmental variable. I'm not advocating for password hashes or anything like that.

<!-- gh-comment-id:2888536916 --> @debsidian commented on GitHub (May 17, 2025): Maybe I'm miscommunicating here? I'm advocating for basic security practices. The ones that are baked into Docker's own documentation: > A secret is any piece of data, such as a password, certificate, or API key, that shouldn’t be transmitted over a network or stored unencrypted in a Dockerfile or in your application’s source code. > > Docker Compose provides a way for you to use secrets without having to use environment variables to store information. If you’re injecting passwords and API keys as environment variables, you risk unintentional information exposure. Services can only access secrets when explicitly granted by a secrets attribute within the services top-level element. > > Environment variables are often available to all processes, and it can be difficult to track access. They can also be printed in logs when debugging errors without your knowledge. Using secrets mitigates these risks. > > [Source](https://docs.docker.com/compose/how-tos/use-secrets/) No third party links necessary. Docker tells you what best practices are. I'm just asking that they be implemented here. It's okay if the answer is 'no', but I figured it couldn't hurt to ask. Edit: Just to be clear, the ask is simply to mount/link to a file within the `docker-compose` instead of putting the api key as a plaintext environmental variable. I'm not advocating for password hashes or anything like that.
Author
Owner

@MohamedBassem commented on GitHub (May 17, 2025):

@debsidian I think this is a reasonable ask.

<!-- gh-comment-id:2888544415 --> @MohamedBassem commented on GitHub (May 17, 2025): @debsidian I think this is a reasonable ask.
Author
Owner

@Eragos commented on GitHub (May 17, 2025):

@debsidian Yes, I didn't know that. Interesting Link - thank you!

Best Michael

<!-- gh-comment-id:2888548748 --> @Eragos commented on GitHub (May 17, 2025): @debsidian Yes, I didn't know that. Interesting Link - thank you! Best Michael
Author
Owner

@thiswillbeyourgithub commented on GitHub (May 19, 2025):

What do we think about encryption here? The sqlite db does not seem to use any "encrypted at rest" feature (meaning the storage provider can read the data) nor any "per user encryption" (meaning the admin car read everyone's data). Is that something that might change in the future?

Looking a bit at the backend I see that karakeep seems to use drizzle-orm with better-sqlite3, and apparently drizzle can also use libsql which supports encrypted at rest features.

In an idea world, karakeep would not allow the admin to read other user's data, nor the storage provider. IIRC there are some pretty standard and robust ways to do that nowadays with minimal performance tradeoff on normal hardware, no?

I do understand that no matter the encryption of the db, the search engine (currently meilisearch) stores things related to full text search so there is probably some leakage here, and also stores embeddings which can actually be reversed back to text in some situation, but:
1 it would take significant effort to reverse the meilisearch, so blocking that would only be relevant for people that are bigger problems than their bookmarks leak
2. this effort means it should not be doable at scale and in an "indiscriminate nsa dragnet" scenario.
3. Using binary embeddings (#1315) would probably make reversal impossible

All in all, I am interested in the owner's take on encryption for karakeep. I am sure there are low-cost low-risk high-gain things to do, no?

If you want I can open an issue to track this request.

Edit:

Edit:
I guess another way to do encryption could simply be for the host root to mount an encrypted partition on which to store the db files. This could be a sort of valid solution to the storage provider I guess. Not to the curious karakeep administrator account though.

Edit:
I'll make a doc PR to mention encryption based on your answer @MohamedBassem

edit: decided to make the PR anyway: #1479

<!-- gh-comment-id:2890758833 --> @thiswillbeyourgithub commented on GitHub (May 19, 2025): What do we think about encryption here? The sqlite db does not seem to use any "encrypted at rest" feature (meaning the storage provider can read the data) nor any "per user encryption" (meaning the admin car read everyone's data). Is that something that might change in the future? Looking a bit at the backend I see that karakeep seems to use `drizzle-orm` with `better-sqlite3`, and apparently drizzle can [also use libsql which supports encrypted at rest features](https://orm.drizzle.team/docs/get-started-sqlite). In an idea world, karakeep would not allow the admin to read other user's data, nor the storage provider. IIRC there are some pretty standard and robust ways to do that nowadays with minimal performance tradeoff on normal hardware, no? I do understand that no matter the encryption of the db, the search engine (currently meilisearch) stores things related to full text search so there is probably some leakage here, and also stores embeddings which can [actually be reversed back to text in some situation](https://simonwillison.net/2024/Jan/8/text-embeddings-reveal-almost-as-much-as-text/), but: 1 it would take significant effort to reverse the meilisearch, so blocking that would only be relevant for people that are bigger problems than their bookmarks leak 2. this effort means it should not be doable at scale and in an "indiscriminate nsa dragnet" scenario. 3. Using binary embeddings (#1315) would probably make reversal impossible All in all, I am interested in the owner's take on encryption for karakeep. I am sure there are low-cost low-risk high-gain things to do, no? If you want I can open an issue to track this request. Edit: - Meilisearch uses LMDB and mentionned in 2022 being interested in upgrading to the LMDB3 that supports encryption apparently: https://github.com/meilisearch/meilisearch/issues/2570 - Related discusion on meilisearch: https://github.com/meilisearch/meilisearch/issues/5036 Edit: I guess another way to do encryption could simply be for the host root to mount an encrypted partition on which to store the db files. This could be a sort of valid solution to the storage provider I guess. Not to the curious karakeep administrator account though. Edit: I'll make a doc PR to mention encryption based on your answer @MohamedBassem edit: decided to make the PR anyway: #1479
Author
Owner

@Eragos commented on GitHub (May 27, 2025):

Discussion: Feature request: E2E-Encryption or at least server-database encryption has a similar topic.

<!-- gh-comment-id:2912972100 --> @Eragos commented on GitHub (May 27, 2025): Discussion: [Feature request: E2E-Encryption or at least server-database encryption](https://github.com/karakeep-app/karakeep/discussions/1070) has a similar topic.
Author
Owner

@kennethso168 commented on GitHub (Jan 30, 2026):

Just to add some insight. I currently worked around this by overriding the entrypoint of the docker images in my compose files

services:
  ...
  web:
    image: ghcr.io/karakeep-app/karakeep:0.30.0
    restart: unless-stopped
    ...
    secrets:
      - karakeep_oauth_client_secret
      - karakeep_meili_master_key
      - karakeep_nextauth_secret
    entrypoint: ['/bin/sh', '-c', 'export OAUTH_CLIENT_SECRET=$$(cat /run/secrets/karakeep_oauth_client_secret); export MEILI_MASTER_KEY=$$(cat /run/secrets/karakeep_meili_master_key); export NEXTAUTH_SECRET=$$(cat /run/secrets/karakeep_nextauth_secret); /init']

  meilisearch:
    image: getmeili/meilisearch:v1.13.3
    restart: unless-stopped
    ...
    secrets:
      - karakeep_meili_master_key
    entrypoint: ['/bin/sh', '-c']
    command: "'export MEILI_MASTER_KEY=$$(cat /run/secrets/karakeep_meili_master_key); tini -- /bin/meilisearch'"

But this should be considered as a "hack" and it would be great if the containers support secrets in file natively.

<!-- gh-comment-id:3824840618 --> @kennethso168 commented on GitHub (Jan 30, 2026): Just to add some insight. I currently worked around this by overriding the entrypoint of the docker images in my compose files ``` services: ... web: image: ghcr.io/karakeep-app/karakeep:0.30.0 restart: unless-stopped ... secrets: - karakeep_oauth_client_secret - karakeep_meili_master_key - karakeep_nextauth_secret entrypoint: ['/bin/sh', '-c', 'export OAUTH_CLIENT_SECRET=$$(cat /run/secrets/karakeep_oauth_client_secret); export MEILI_MASTER_KEY=$$(cat /run/secrets/karakeep_meili_master_key); export NEXTAUTH_SECRET=$$(cat /run/secrets/karakeep_nextauth_secret); /init'] meilisearch: image: getmeili/meilisearch:v1.13.3 restart: unless-stopped ... secrets: - karakeep_meili_master_key entrypoint: ['/bin/sh', '-c'] command: "'export MEILI_MASTER_KEY=$$(cat /run/secrets/karakeep_meili_master_key); tini -- /bin/meilisearch'" ``` But this should be considered as a "hack" and it would be great if the containers support secrets in file natively.
Author
Owner

@kennethso168 commented on GitHub (Mar 1, 2026):

services:
  ...
  web:
    image: ghcr.io/karakeep-app/karakeep:0.30.0
    restart: unless-stopped
    ...
    secrets:
      - karakeep_oauth_client_secret
      - karakeep_meili_master_key
      - karakeep_nextauth_secret
    entrypoint: ['/bin/sh', '-c', 'export OAUTH_CLIENT_SECRET=$$(cat /run/secrets/karakeep_oauth_client_secret); export MEILI_MASTER_KEY=$$(cat /run/secrets/karakeep_meili_master_key); export NEXTAUTH_SECRET=$$(cat /run/secrets/karakeep_nextauth_secret); /init']

Unfortunately the above "hack" no longer works for 0.31.0. When the entrypoint is overridden, the following error occurs

s6-overlay-suexec: fatal: can only run as pid 1

However, as the karakeep container uses s6-overlay, after spending some time to study s6-overlay, it is possible to add a init script to load the contents of the file defined by FILE__MY_VAR into a new environment variable MY_VAR, just like linuxserver.io images. In fact, I adapted the script from linuxserver.io base images, removing bashisms as the karakeep container does not have bash.

In the folder containing the karakeep compose file, create the following folder structure:

karakeep
├── compose.yml
├── empty
└── init-envfile
    ├── run
    ├── type
    └── up

The file empty, as the name implies, is empty.

And for the remaining files:

init-envfile/run:

#!/command/with-contenv sh
# shellcheck shell=bash

if find /run/s6/container_environment/FILE__* -maxdepth 1 > /dev/null 2>&1; then
    for FILENAME in /run/s6/container_environment/FILE__*; do
            SECRETFILE=$(cat "${FILENAME}")
            if [ -f ${SECRETFILE} ]; then
                FILESTRIP="$(echo ${FILENAME} | sed 's|FILE__||')"
                if [ $(tail -n1 "${SECRETFILE}" | wc -l) != 0 ]; then
                    echo "[init-envfile] Your secret: $(basename ${FILENAME})"
                    echo "           contains a trailing newline and may not work as expected"
                fi
                cat "${SECRETFILE}" >"${FILESTRIP}"
                echo "[init-envfile] $(basename ${FILESTRIP}) set from $(basename ${FILENAME})"
            else
                echo "[init-envfile] cannot find secret in $(basename ${FILENAME})"
            fi
    done
fi

init-envfile/type

oneshot

init-envfile/up

/etc/s6-overlay/s6-rc.d/init-envfile/run

Make all three files inside the init-envfile folder executable

Then, we can mount the files into the container and set our environment variables accordingly:

services:
  ...
  web:
    image: ghcr.io/karakeep-app/karakeep:0.31.0
    restart: unless-stopped
    volumes:
      - /path/to/your/directory:/data
      - ./init-envfile:/etc/s6-overlay/s6-rc.d/init-envfile
      - ./empty:/etc/s6-overlay/s6-rc.d/init-db-migration/dependencies.d/init-envfile
      - ./empty:/etc/s6-overlay/s6-rc.d/user/contents.d/init-envfile
    ...
    environment:
      ...
      FILE__OAUTH_CLIENT_SECRET: '/run/secrets/karakeep_oauth_client_secret'
      FILE__MEILI_MASTER_KEY: '/run/secrets/karakeep_meili_master_key'
      FILE__NEXTAUTH_SECRET: '/run/secrets/karakeep_nextauth_secret'
    secrets:
      - karakeep_oauth_client_secret
      - karakeep_meili_master_key
      - karakeep_nextauth_secret

Example container logs with the above modification:

s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service init-envfile: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service legacy-cont-init: starting
s6-rc: info: service legacy-cont-init successfully started
[init-envfile] MEILI_MASTER_KEY set from FILE__MEILI_MASTER_KEY
[init-envfile] NEXTAUTH_SECRET set from FILE__NEXTAUTH_SECRET
[init-envfile] OAUTH_CLIENT_SECRET set from FILE__OAUTH_CLIENT_SECRET
s6-rc: info: service init-envfile successfully started
s6-rc: info: service init-db-migration: starting
Running db migration script
s6-rc: info: service init-db-migration successfully started
s6-rc: info: service svc-workers: starting
s6-rc: info: service svc-web: starting
s6-rc: info: service svc-workers successfully started
s6-rc: info: service svc-web successfully started
s6-rc: info: service legacy-services: starting
s6-rc: info: service legacy-services successfully started
   ▲ Next.js 15.3.8
   - Local:        http://localhost:3000
   - Network:      http://0.0.0.0:3000

 ✓ Starting...
2026-03-01T15:21:23.283Z info: Tracing is disabled
 ✓ Ready in 1144ms

Maybe I can open a PR to add the above init script into the image

<!-- gh-comment-id:3980311803 --> @kennethso168 commented on GitHub (Mar 1, 2026): > ``` > services: > ... > web: > image: ghcr.io/karakeep-app/karakeep:0.30.0 > restart: unless-stopped > ... > secrets: > - karakeep_oauth_client_secret > - karakeep_meili_master_key > - karakeep_nextauth_secret > entrypoint: ['/bin/sh', '-c', 'export OAUTH_CLIENT_SECRET=$$(cat /run/secrets/karakeep_oauth_client_secret); export MEILI_MASTER_KEY=$$(cat /run/secrets/karakeep_meili_master_key); export NEXTAUTH_SECRET=$$(cat /run/secrets/karakeep_nextauth_secret); /init'] > ``` Unfortunately the above "hack" no longer works for 0.31.0. When the entrypoint is overridden, the following error occurs ``` s6-overlay-suexec: fatal: can only run as pid 1 ``` However, as the karakeep container uses `s6-overlay`, after spending some time to study `s6-overlay`, it is possible to add a init script to load the contents of the file defined by `FILE__MY_VAR` into a new environment variable `MY_VAR`, just like `linuxserver.io` images. In fact, I adapted the script from `linuxserver.io` base images, removing bashisms as the karakeep container does not have bash. In the folder containing the karakeep compose file, create the following folder structure: ``` karakeep ├── compose.yml ├── empty └── init-envfile ├── run ├── type └── up ``` The file `empty`, as the name implies, is empty. And for the remaining files: `init-envfile/run`: ```bash #!/command/with-contenv sh # shellcheck shell=bash if find /run/s6/container_environment/FILE__* -maxdepth 1 > /dev/null 2>&1; then for FILENAME in /run/s6/container_environment/FILE__*; do SECRETFILE=$(cat "${FILENAME}") if [ -f ${SECRETFILE} ]; then FILESTRIP="$(echo ${FILENAME} | sed 's|FILE__||')" if [ $(tail -n1 "${SECRETFILE}" | wc -l) != 0 ]; then echo "[init-envfile] Your secret: $(basename ${FILENAME})" echo " contains a trailing newline and may not work as expected" fi cat "${SECRETFILE}" >"${FILESTRIP}" echo "[init-envfile] $(basename ${FILESTRIP}) set from $(basename ${FILENAME})" else echo "[init-envfile] cannot find secret in $(basename ${FILENAME})" fi done fi ``` `init-envfile/type` ``` oneshot ``` `init-envfile/up` ``` /etc/s6-overlay/s6-rc.d/init-envfile/run ``` Make all three files inside the `init-envfile` folder executable Then, we can mount the files into the container and set our environment variables accordingly: ``` services: ... web: image: ghcr.io/karakeep-app/karakeep:0.31.0 restart: unless-stopped volumes: - /path/to/your/directory:/data - ./init-envfile:/etc/s6-overlay/s6-rc.d/init-envfile - ./empty:/etc/s6-overlay/s6-rc.d/init-db-migration/dependencies.d/init-envfile - ./empty:/etc/s6-overlay/s6-rc.d/user/contents.d/init-envfile ... environment: ... FILE__OAUTH_CLIENT_SECRET: '/run/secrets/karakeep_oauth_client_secret' FILE__MEILI_MASTER_KEY: '/run/secrets/karakeep_meili_master_key' FILE__NEXTAUTH_SECRET: '/run/secrets/karakeep_nextauth_secret' secrets: - karakeep_oauth_client_secret - karakeep_meili_master_key - karakeep_nextauth_secret ``` Example container logs with the above modification: ``` s6-rc: info: service s6rc-oneshot-runner: starting s6-rc: info: service s6rc-oneshot-runner successfully started s6-rc: info: service fix-attrs: starting s6-rc: info: service init-envfile: starting s6-rc: info: service fix-attrs successfully started s6-rc: info: service legacy-cont-init: starting s6-rc: info: service legacy-cont-init successfully started [init-envfile] MEILI_MASTER_KEY set from FILE__MEILI_MASTER_KEY [init-envfile] NEXTAUTH_SECRET set from FILE__NEXTAUTH_SECRET [init-envfile] OAUTH_CLIENT_SECRET set from FILE__OAUTH_CLIENT_SECRET s6-rc: info: service init-envfile successfully started s6-rc: info: service init-db-migration: starting Running db migration script s6-rc: info: service init-db-migration successfully started s6-rc: info: service svc-workers: starting s6-rc: info: service svc-web: starting s6-rc: info: service svc-workers successfully started s6-rc: info: service svc-web successfully started s6-rc: info: service legacy-services: starting s6-rc: info: service legacy-services successfully started ▲ Next.js 15.3.8 - Local: http://localhost:3000 - Network: http://0.0.0.0:3000 ✓ Starting... 2026-03-01T15:21:23.283Z info: Tracing is disabled ✓ Ready in 1144ms ``` Maybe I can open a PR to add the above init script into the image
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/karakeep#895
No description provided.