My painful journey of group permissions to make native 1Password work in devcontainers
So... I distrohopped. I thought I settled on nixos and I actually quite like it, but it has been a bit... lot lately.
Fedora silverblue has been great! Everything mostly just works, and I have the same piece of mind of a broken update not forcing me to reach for a live CD.
But there is a drawback. It, along with all other non-Nix distros (I can think of) doesn't allow you to have a native, per-project environment.
Why native?
1Password. If you haven't used it it is really nice. It fixes the age-old issue of committing your secrets by making it almost impossible to do so.
Instead of hard-coding your secrets, you just load an env with secrets references!
export MY_GITHUB_EMAIL="op://Personal/GitHub/username" op run --no-masking -- printenv MY_GITHUB_EMAIL # Requests confirmation from desktop app tc001@t0.lv
This is super powerful. You can combine it with a template env file to make a setup that works across machines and distros, as long as you can get the 1Password CLI on it.
PATREON_CLIENT_ID="op://Project1/Patreon/Client ID" PATREON_CLIENT_SECRET="op://Project1/Patreon/Client secret" PATREON_CREATOR_TOKEN="op://Project1/Patreon/Creator token" PATREON_CREATOR_REFRESH="op://Project1/Patreon/Creator refresh" DISCORD_CLIENT_ID="op://Project1/Discord bot/Bot ID" DISCORD_CLIENT_SECRET="op://Project1/Discord bot/Bot client secret" DISCORD_BOT_TOKEN="op://Project1/Discord bot/Bot token"
Ok enough free advertisment (1passwordplesesponsorme), because now it is time to talk about my issues.
Devcontainers just don't work
Well they do, but 1Password inside them doesn't. Adding the `op` command was easy enough. Just
COPY --from=docker.io/1password/op:2 /usr/local/bin/op /usr/local/bin/op
but that doesn't integrate with the desktop app. And that is by design! Because a sandbox with default access to your passwords is not a great sandbox.
Easy enough fix, just find how they communicate and allow that!
{
"source": "${env:XDG_RUNTIME_DIR}/1Password-BrowserSupport.sock",
"target": "${env:XDG_RUNTIME_DIR}/1Password-BrowserSupport.sock",
"type": "bind"
}But that wasn't enough...
op account list --debug 6:45PM | DEBUG | Skipped loading desktop app settings file. The desktop app might not be installed: read file: lstat /home/node/.config/1Password/settings/settings.json: no such file or directory
...just expose the 1Password settings as well!
{
"source": "${env:HOME}/.config/1Password/settings/",
"target": "/home/node/.config/1Password/settings/",
"type": "bind"
}Aaaand now it has a new error
op account list --debug [ERROR] 2025/10/19 18:54:34 connecting to desktop app: cannot connect to 1Password app, make sure it is running
This time the mistake was ${env:XDG_RUNTIME_DIR}, turns out in the container /run/user/1000 is instead /tmp/user/1000 because of some systemd shenanigans.
I just fixed it by making the mount target /tmp/user/1000/
With that fixed I can finally see... another error:
$ op account list --debug 6:59PM | WARN | no enforced policies found error="reading policies file: lstat /home/node/.config/1Password/settings/policies.json: no such file or directory" 6:59PM | DEBUG | Session delegation enabled 6:59PM | DEBUG | NM request: NmRequestAccounts [ERROR] 2025/10/19 18:59:10 connecting to desktop app: read: connection reset, make sure 1Password CLI is installed correctly, then open the 1Password app, select 1Password > Settings > Developer and make sure the 'Integrate with 1Password CLI' setting is turned on. If you're still having trouble connecting, visit https://developer.1password.com/docs/cli/app-integration#troubleshooting for more help.
Now this is fun! From the host logs I can see a connection is being made, but rejected.
INFO 2025-10-19T18:59:10.260+00:00 runtime-worker(ThreadId(15)) [1P:native-messaging/op-native-core-integration/src/lib.rs:387] Extension connecting. ERROR 2025-10-19T18:59:10.260+00:00 runtime-worker(ThreadId(15)) [1P:native-messaging/op-native-core-integration/src/lib.rs:667] Failed to accept new connection.: PipeAuthError(NoCreds) ERROR 2025-10-19T19:01:11.176+00:00 runtime-worker(ThreadId(15)) [1P:op-ipc/src/ipc/unix.rs:413] peer was not in the correct application group, rejecting remote
I will spare the 4 hours of troubleshooting (turns out devcontainers was overriding groups IDs), but the secret was the group 1001, aka onepassword-cli
The docs describe it as
1Password CLI connects to a Unix socket opened by the 1Password app. The socket is owned by the current user/group, allowing any process started by this user to connect to it. 1Password CLI is owned by the onepassword-cli group and has the set-gid bit set on Linux. The 1Password app verifies the authenticity of 1Password CLI by checking if the GID of the process connecting on the unix socket is equal to that of the onepassword-cli group. If the GID doesn't match, the connection is reset before any messages are accepted.
✨ just suggested the same config change in different ways. What finally worked was having my primary group 1001 with "remoteUser": "node:1001". (--user was getting overwritten)
But I knew I could do better! A good sleep and a fresh ✨ immediately suggested the fix that was in the docs all along: the set-gid bit.
Turns out you don't even need the user to have that group, just that op runs as group 1001 and connects to the socket as that!
So a quick dockerfile later I had finally done it! 1Password works in a devcontainer.
It works!
FROM mcr.microsoft.com/devcontainers/typescript-node:24
COPY --from=docker.io/1password/op:2 /usr/local/bin/op /usr/local/bin/op
USER root
# Set up the 1Password-cli group and permissions
RUN groupadd -r -g 1001 onepassword-cli
RUN chown root:onepassword-cli /usr/local/bin/op \
&& chmod g+s /usr/local/bin/op
RUN usermod -aG onepassword-cli node
# Set up XDG_RUNTIME_DIR
# This is implicitly already set, but I like to be explicit about it because it is used for a bind
ENV XDG_RUNTIME_DIR=/tmp/user/1000
# Change the ownership because vscode server tries to set up a wayland socket here
RUN mkdir -p /tmp/user/1000 \
&& chown -R node:node /tmp/user/1000
USER node{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"runArgs": [],
"mounts": [
{
"source": "${env:HOME}/.config/1Password/settings/",
"target": "/home/node/.config/1Password/settings/",
"type": "bind"
},
{
"source": "${env:XDG_RUNTIME_DIR}/1Password-BrowserSupport.sock",
"target": "/tmp/user/1000/1Password-BrowserSupport.sock",
"type": "bind"
}
]
}$ op account list URL EMAIL USER ID # ...
The 1Password shaped hole in the sandbox
Yes, this is way less secure than an airtight devcontainer. A malicious application can use the non-protected sudo to get the group or just directly call op to connect to the socket.
But I would argue it is at least as secure as executing the same code natively (like with nix shell), but it can only access the 1Password socket and not the rest of the filesystem. 1Password still requires an in-app confirmation popup before it will even allow a secret to be read from the CLI, and the vault is not exposed to the container, so malware can't steal that and decrypt it offline.