Why 1Password Connect?
I use 1Password as password manager for my personal use. I was wondering if I already have it, can I use it somehow on my server? And I was read some documents and I have seen that 1Password provides multiple way to integrate it into the infrastructure. My plan is to sync secrets from 1Password vault as Podman secrets.
1Password Connect has two components:
Sync: This part synchronize data from 1Password server to locally so the fetch is faster.API: This part host the synchronized file and make a local REST API to be able to fetch (and modify if needed) them
Something like this, in the following figure.
馃挕 Tip
Before you start, you must create a 1Password Connect server and tokens. 1Password document describes how to do it: https://developer.1password.com/docs/connect/get-started#step-1Install with Docker
Based on 1Password guide, the following compose file can be used with Docker. This will be the base of my implementation with Quadlet.
version: "3.4"
services:
op-connect-api:
image: 1password/connect-api:latest
ports:
- "8080:8080"
volumes:
- "./1password-credentials.json:/home/opuser/.op/1password-credentials.json"
- "data:/home/opuser/.op/data"
op-connect-sync:
image: 1password/connect-sync:latest
ports:
- "8081:8080"
volumes:
- "./1password-credentials.json:/home/opuser/.op/1password-credentials.json"
- "data:/home/opuser/.op/data"
volumes:
data:
Implementation in Quadlet
First, I summarize the key changes comparing with compose file:
- I put both into one common pod. This makes network configuration easier, but brings more challenge, like they cannot use the same port (8080).
- Credentials are stored in Podman secret.
Create the pod
Start with the simplest thing, create a pod. I created a file with 1pw.pod
name.
Programs that would use it, will run on my host system, not in container.
So, to make it available for them, then a port must be published.
I also bind it to 127.0.0.1 on local system, to prevent that anything would
reach it from other system on the network.
[Pod]
PodName=1password
PublishPort=127.0.0.1:8000:8000
The volume unit file, called 1pw.volume is really simple:
[Volume]
Label=app=1pw
Configure Sync and API
Both configuration seems very similar. In the documentation,
it can be seen that ports can be re-configured using OP_HTTP_PORT.
But both cannot use the same port, since they are in one pod, so they share the
network interface.
Sync and API must be connect to each other. It is not in Docker configuration
because it is there on implicit way. The OP_BUS_PORT also has to be set.
And the other’s service bus port must be set as OP_BUS_PEERS.
Here is a table to summarize the ports:
| Port type | Sync service | API service |
|---|---|---|
| HTTP port | 8001 | 8000 |
| BUS port | 9001 | 9000 |
| BUS peer address | 127.0.0.1:9000 | 127.0.0.1:9001 |
For configuration file, I have read the config and convert it as Podman secret. In the unit file, these secrets mounted as files.
cat 1password-cred.json | podman secret create 1pw-cred-file -
rm 1password-cred.json
And here are the unit files: 1pw-sync.container and 1pw-api.container.
[Unit]
Description=1Password Connect Sync
[Container]
Pod=1pw.pod
Image=docker.io/1password/connect-sync:latest
AutoUpdate=registry
# Persistent volumes
Volume=1pw.volume:/home/opuser/.op/data
# Environment variables
Environment=OP_HTTP_PORT=8001
Environment=OP_BUS_PORT=9001
Environment=OP_BUS_PEERS=127.0.0.1:9000
Secret=1pw-cred_file,target=/home/opuser/.op/1password-credentials.json
# Other
UserNS=keep-id
LogDriver=journald
[Service]
Restart=on-failure
RestartSec=5
StartLimitBurst=5
[Install]
WantedBy=default.target
[Unit]
Description=1Password Connect API
[Container]
Pod=1pw.pod
Image=docker.io/1password/connect-api:latest
AutoUpdate=registry
# Persistent volumes
Volume=1pw.volume:/home/opuser/.op/data
# Environment variables
Environment=OP_HTTP_PORT=8000
Environment=OP_BUS_PORT=9000
Environment=OP_BUS_PEERS=127.0.0.1:9001
Secret=1pw-cred_file,target=/home/opuser/.op/1password-credentials.json
# Other
UserNS=keep-id
LogDriver=journald
[Service]
Restart=on-failure
RestartSec=5
StartLimitBurst=5
[Install]
WantedBy=default.target
Start and test it
It can be simply start by systemctl --user start 1pw-pod command.
After it has been started, resources are created.
# List created containers
$ podman ps --pod --filter label=app=1pw
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES POD ID PODNAME
d28def41e253 docker.io/1password/connect-api:latest 50 seconds ago Up 50 seconds 127.0.0.1:8000->8000/tcp, 8080/tcp, 8443/tcp systemd-1pw-api b8fd385e2b0b 1password
d77ed38d745d docker.io/1password/connect-sync:latest 50 seconds ago Up 50 seconds 127.0.0.1:8000->8000/tcp systemd-1pw-sync b8fd385e2b0b 1password
# List created volumes
$ podman volume ls --filter label=app=1pw
DRIVER VOLUME NAME
local systemd-1pw
# List generated unit files
$ systemctl --user --type=service | grep 1pw
1pw-api.service loaded active running 1Password Connect API
1pw-pod.service loaded active running 1pw-pod.service
1pw-sync.service loaded active running 1Password Connect Sync
1pw-volume.service loaded active exited 1pw-volume.service
# I use 1Password CLI to fetch access token from my 1Password. This env variable is evaluated using `op run` command later
# If you don't use 1Password CLI (it is independent from this connect), then just specify token here
$ export API_TOKEN="op://my_vault/connect-access-token/credential"
# Issue a simple GET with `curl`, command `jq` format it nicely
$ op run --no-masking -- bash -c \
'curl -s -H "Accept: application/json" -H "Authorization: Bearer $API_TOKEN" http://localhost:8000/v1/vaults' | jq .
[
{
"attributeVersion": 1,
"contentVersion": 18,
"createdAt": "9999-12-31T23:59:59Z",
"id": "vaultidjsdlfkjsdklf",
"items": 8,
"name": "myvault",
"type": "USER_CREATED",
"updatedAt": "9999-12-31T23:59:59Z"
}
]
Final thoughts
Integration of 1Password, including CLI, Connect and K8S operator, seems really nice. Currently it does not have feature to sync secrets into podman secret, but this is something that I can do for myself. My idea is to make a program that check if item has been updated in 1Password and if it is, then replace the Podman secret and restart container if needed.