Add 1Password Connect support for secrets
All checks were successful
Build and Push Container / build (push) Successful in 31s
All checks were successful
Build and Push Container / build (push) Successful in 31s
Secrets can now use op:// references which are resolved via 1Password Connect API at startup. Set OP_CONNECT_HOST and OP_CONNECT_TOKEN to enable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,69 @@
|
|||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_from_op_connect(op_ref: str, connect_host: str, connect_token: str) -> str:
|
||||||
|
"""Fetch a secret from 1Password Connect using op:// reference."""
|
||||||
|
match = re.match(r"op://([^/]+)/([^/]+)/(.+)", op_ref)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid 1Password reference: {op_ref}")
|
||||||
|
|
||||||
|
vault_name, item_name, field_name = match.groups()
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {connect_token}"}
|
||||||
|
|
||||||
|
# Get vault ID
|
||||||
|
vaults_resp = requests.get(f"{connect_host}/v1/vaults", headers=headers)
|
||||||
|
vaults_resp.raise_for_status()
|
||||||
|
vault_id = next((v["id"] for v in vaults_resp.json() if v["name"] == vault_name), None)
|
||||||
|
if not vault_id:
|
||||||
|
raise ValueError(f"Vault not found: {vault_name}")
|
||||||
|
|
||||||
|
# Get item
|
||||||
|
items_resp = requests.get(
|
||||||
|
f"{connect_host}/v1/vaults/{vault_id}/items",
|
||||||
|
headers=headers,
|
||||||
|
params={"filter": f'title eq "{item_name}"'}
|
||||||
|
)
|
||||||
|
items_resp.raise_for_status()
|
||||||
|
items = items_resp.json()
|
||||||
|
if not items:
|
||||||
|
raise ValueError(f"Item not found: {item_name}")
|
||||||
|
|
||||||
|
# Get item details with fields
|
||||||
|
item_resp = requests.get(
|
||||||
|
f"{connect_host}/v1/vaults/{vault_id}/items/{items[0]['id']}",
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
item_resp.raise_for_status()
|
||||||
|
item = item_resp.json()
|
||||||
|
|
||||||
|
# Find field
|
||||||
|
for field in item.get("fields", []):
|
||||||
|
if field.get("label", "").lower() == field_name.lower() or field.get("id", "").lower() == field_name.lower():
|
||||||
|
return field.get("value", "")
|
||||||
|
|
||||||
|
raise ValueError(f"Field not found: {field_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_secret(env_var: str, default: str = "") -> str:
|
||||||
|
"""Resolve a secret from env var or 1Password Connect."""
|
||||||
|
value = os.environ.get(env_var, default)
|
||||||
|
|
||||||
|
if value.startswith("op://"):
|
||||||
|
connect_host = os.environ.get("OP_CONNECT_HOST")
|
||||||
|
connect_token = os.environ.get("OP_CONNECT_TOKEN")
|
||||||
|
if not connect_host or not connect_token:
|
||||||
|
raise ValueError(f"{env_var} uses op:// reference but OP_CONNECT_HOST/OP_CONNECT_TOKEN not set")
|
||||||
|
return fetch_from_op_connect(value, connect_host, connect_token)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
"""Application configuration loaded from environment variables."""
|
"""Application configuration loaded from environment variables."""
|
||||||
@@ -17,7 +77,7 @@ class Config:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls) -> "Config":
|
def from_env(cls) -> "Config":
|
||||||
"""Load configuration from environment variables."""
|
"""Load configuration from environment variables."""
|
||||||
ssh_key_b64 = os.environ.get("SSH_PRIVATE_KEY", "")
|
ssh_key_b64 = resolve_secret("SSH_PRIVATE_KEY", "")
|
||||||
|
|
||||||
# Decode base64 SSH key
|
# Decode base64 SSH key
|
||||||
try:
|
try:
|
||||||
@@ -28,8 +88,8 @@ class Config:
|
|||||||
return cls(
|
return cls(
|
||||||
ssh_private_key=ssh_private_key,
|
ssh_private_key=ssh_private_key,
|
||||||
uptime_kuma_url=os.environ.get("UPTIME_KUMA_URL", "http://localhost:3001"),
|
uptime_kuma_url=os.environ.get("UPTIME_KUMA_URL", "http://localhost:3001"),
|
||||||
uptime_kuma_api_key=os.environ.get("UPTIME_KUMA_API_KEY", ""),
|
uptime_kuma_api_key=resolve_secret("UPTIME_KUMA_API_KEY", ""),
|
||||||
claude_api_key=os.environ.get("CLAUDE_API_KEY", ""),
|
claude_api_key=resolve_secret("CLAUDE_API_KEY", ""),
|
||||||
dev_mode=os.environ.get("DEV_MODE", "false").lower() == "true",
|
dev_mode=os.environ.get("DEV_MODE", "false").lower() == "true",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user