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.

graph TD A(1Password) --> Sync subgraph Local subgraph 1Password Connect Sync --> API API end API --> P1(Process #1) API --> P2(Process #2) API --> P3(Process #3) API --> P4(Process #4) end

馃挕 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-1

Install 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 typeSync serviceAPI service
HTTP port80018000
BUS port90019000
BUS peer address127.0.0.1:9000127.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.