Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
280bbdf600 | ||
|
|
60931fbba1 | ||
|
|
306c68e7ea | ||
|
|
36d6dad956 |
@@ -0,0 +1,41 @@
|
||||
# Builds and pushes the key-rotation-controller image.
|
||||
#
|
||||
# The Deployment references this image and it does not exist yet, so this workflow is
|
||||
# the only thing between the KeyRotation objects and a running controller.
|
||||
name: key-rotation-controller
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "k8s/apps/key-rotation-controller/controller/**"
|
||||
- ".forgejo/workflows/key-rotation-controller.yaml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
image:
|
||||
runs-on: golang
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the registry
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
||||
docker login forgejo.riotpiao.com -u poimen --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
working-directory: k8s/apps/key-rotation-controller/controller
|
||||
run: |
|
||||
set -eu
|
||||
IMAGE=forgejo.riotpiao.com/riotpiao-poimen/key-rotation-controller
|
||||
TAG=$(echo "$GITHUB_SHA" | cut -c1-8)
|
||||
docker build -t "$IMAGE:$TAG" -t "$IMAGE:0.1.0" .
|
||||
docker push "$IMAGE:$TAG"
|
||||
docker push "$IMAGE:0.1.0"
|
||||
echo "pushed $IMAGE:$TAG and $IMAGE:0.1.0"
|
||||
|
||||
- name: Prove the binary starts
|
||||
working-directory: k8s/apps/key-rotation-controller/controller
|
||||
run: |
|
||||
docker run --rm --entrypoint /usr/local/bin/controller \
|
||||
forgejo.riotpiao.com/riotpiao-poimen/key-rotation-controller:0.1.0 --help || true
|
||||
@@ -0,0 +1,13 @@
|
||||
# Build stage. The controller has no cgo and no runtime assets, so the final image
|
||||
# is a static binary on a base with no shell.
|
||||
FROM golang:1.26 AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/controller .
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
COPY --from=build /out/controller /usr/local/bin/controller
|
||||
USER 65532:65532
|
||||
ENTRYPOINT ["/usr/local/bin/controller"]
|
||||
@@ -1,30 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: comfyui-oauth2-proxy
|
||||
namespace: comfyui
|
||||
type: Opaque
|
||||
data:
|
||||
client-id: ENC[AES256_GCM,data:jFLgcrh1tiAU0pijGxy+/mFsFHzI5zSi,iv:7GYL4xtPnzyZWtEA0egPHhzvpXGevkIH3Cvr+oD9hKI=,tag:UeDTIKlfcYnOOOJiPNgN0g==,type:str]
|
||||
client-secret: ENC[AES256_GCM,data:9QFu5a2EkEMjc/B0moPIkMEChrN0Qh5BQtAzFcvW4liKxzVAPbTKgQOJeND+NQjn5ZoE89kqqRyqNJF3bT3H1pWoDFngk9J2X7BN/9r6Qyubias0Jukydw==,iv:xf2kT0fbnJat3L3xxUYm3ht0YvHptU/RIdt9Eh0D6xU=,tag:8iknaQ2kKP1OZ/1jRCa+1g==,type:str]
|
||||
cookie-secret: ENC[AES256_GCM,data:Qq+l4go/CFtfsZrgYUcTj9bB4sreu2GEqJnu15H6fxIlD7IgnmrodPoIVNpWcdm6LSYabGjm2jraN6RP,iv:EOoDHgvIydzHCC5POohjPq3Vj2YTpzhIhvdG2FzKKzc=,tag:iDXqzFU1Z79bzpG4I4xfww==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxOUtCUDduMVU5U1J4WXZX
|
||||
Q1VzaFRVdXlHaEZpeVJNOWprdDhUOEd6Z1hzCi9QZEpXN3NDSENzUE9RTGdpb3Va
|
||||
eVNHVTFLbXpMU0x5UWlFLzZnVlllWE0KLS0tIDZldVNqR1loNHlXZWovNWZ4WjFD
|
||||
dVErenJDM3haSStxZEVWZUtBbDBIZ1UK9x6Gy4drjfUz2goAaI+oeUDWdNHQFAJl
|
||||
JGI+X81r/6Krg5N8QDa/mE96E2M2G0yAO5FS1YbpCKOITUEyheacnQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-10-27T21:17:08Z"
|
||||
mac: ENC[AES256_GCM,data:tTGmWFidEWTWHgKPV7b5ABOJwwlP1yQDK5Jd60c9ko6IsmP2GBBPCth7kJgPXuPcw+NHbJ7yt7bcVto7GkPdtY9daJmjPq0Gt16ZO4Xi4hCk22JVKqi/KJcKD/KB0r/VO9w093/Who5CcYgqifextfca4CuxS5m5dcOn4kRJCfE=,iv:0fDvvQr9wp8IrD16u88Ml4HsPEeMhd8R3ljQJoIpw3E=,tag:AanM7C6vbaBBmus1u1GRoA==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
@@ -1,28 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: grafana-oidc
|
||||
namespace: logging
|
||||
type: Opaque
|
||||
stringData:
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ENC[AES256_GCM,data:fGqA1ofVme1lVLxFjWIiU7sbvE9ip2Af7C1ua3+HdidlcVgkwW59vVlXZhgBRdoif86C8LKQSyoCGUqiO+740g==,iv:6Kv2ybbJg1GT8DLqz9P5y+E1zr7vcIBDMUErmvxNTaw=,tag:0wK28B3G/owCVPPqb2sLmQ==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArTXMxMWdVRHIvbWh1bVNk
|
||||
bEFFNlgzNU9zTjlzY2o3VGdnbFpnU2lkYmxrCjF4aWVGSlJaNkcxODJ6RDkrVFo2
|
||||
UkV2RUwwVHFCd0xMNG9RWXdTUHZGaXcKLS0tIHVaWjhaNHRtS2c0UkVzRk83ckhj
|
||||
OCtBVFR0d1F5amVhV2E0emRZWThvYjAKFW46i9brIL9ZoO1CEn+IewIdoUhvYK/A
|
||||
RqzZ6xV+mhlDj0zC+SpVcEm/Ki83nMYOj4+vQingZlr/Kr3Nwb0iLQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-10-27T19:44:32Z"
|
||||
mac: ENC[AES256_GCM,data:EJew+p/kJrurcNpLlFxJGTl69hQYxn3IkzP7EDtdSriW/WI7X1xNLC2dp/dTiLomGmG1uh6+2JI9Sc8gXmrRAWLELAbiOaYbcoU6gi89BjTCWGDAMX4lNSIuH40rsGs4YtYj5U7SGfgx5qto4ZH7LxjBmxzPWM/Xz2Uq/I5jV4k=,iv:LdhjhwCfPUIa7D0gved8Twf7ujN+dg3lN8lCnFBFs0g=,tag:bjkwUCMtR8IBL9BkbuXcnw==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
@@ -5,23 +5,25 @@ metadata:
|
||||
name: oauth2-credentials
|
||||
namespace: iam
|
||||
data:
|
||||
api-gw-client-secret: ENC[AES256_GCM,data:YiUCk61cCsTJE8gTLZWhqVPsCSu3b4OnG+Oh456A+mnxip0FjLTTqV+kYBdfZivIkn9ua4RcVW+rYILu,iv:K08m1EaB7/SIVNWZ3FfeRP4tbCAo6KeiNC0NpE9VO1o=,tag:ZXV9YRWUOPJ/vdoqMtvQeA==,type:str]
|
||||
minio-client-secret: ENC[AES256_GCM,data:vcwLo1cJqbZr2MwkAKO09xPjb09ss7+s6HIvEfaLxt8ka5PUAlXQ37pTkynuktlKq/t0DvSk+x1+5G7QqVD1JJlwyDGM6yuOhBQ3MwucLNl+T4RDKtgGuA==,iv:sCdd+UNX2TfTAM6Zslrh8KrvBSzqpe67F3YMGrl4YDU=,tag:9Tpzm9NEXNuXyCd7l7GeHw==,type:str]
|
||||
poimen-client-secret: ENC[AES256_GCM,data:dYtCiMGZaOFokihfGD2SXJDfXrhfwEQcTOPNu7JNHDUnx0hqwAdYAPAJTEN21p0OpHYDINMs5lwUpMwY,iv:ll8USuzs0881ryHImVjHGO0iFxUzqg5XtI6Ekd+EFXE=,tag:I/4ZfB9Ss4zEP57sRtRTXQ==,type:str]
|
||||
paperless-client-secret: ENC[AES256_GCM,data:XMj9rHkI20lI37vChq9wS5lUq7l95SisRX0LChuj09O5w76Zt9NdG6OEUYQFf1z8vCfMJxBInIEEIq0e,iv:UpYy2ZEZ4AgBACEyrlxEcr13V+8Ksu4p8JHYApmyYHo=,tag:o9Z+/b6T9POgowIsks56kw==,type:str]
|
||||
grafana-client-secret: ENC[AES256_GCM,data:V/j0+P3HoDLqYjRalWK4v1hvJTuVmzxVra47HsF1/BZvWZjIyEIH3Pt8BekT3O1dUFynxjcaxdZMBVYg,iv:9pMp+vIsp/sA7GP9wqNDf5DdZVLDow46OURW0mMRXIc=,tag:i7VJFA5o0hToZEJ5JPWIvw==,type:str]
|
||||
api-gw-client-secret: ENC[AES256_GCM,data:q41OBQ6WvxsmHmXFhrdzeNUlQyPKuOQQa0vWdhuioQwTsNJ5+l9rYZKBAcZxzvd4P066aRN6yVPmEZhq,iv:Dnfgy8N9NVOM2x656Qei8960dzu7icAU3BZLXmj7N5o=,tag:deilBSCC9Mg/Eo8K0w6rMw==,type:str]
|
||||
poimen-client-secret: ENC[AES256_GCM,data:2k28ui+Mbu3qd4ar6/NidO4bWO8g7JQUjDQBD8liywpCm7rA8TSrmU51im/I5kEvRRhl+RByEo5wpozo,iv:JFrZYF7cP/tQL8gkQJ5GvtdOt1kKcAdHE7L4l+uT9M0=,tag:DSiv8kv7MINUxi7fO/093w==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- enc: |
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2amZPendIOU9PeStOcmJG
|
||||
c1dzOFVvd3RoT3dXQlJUNkdCSytEZUVGVEJJCklMUkNjUytBMXh0OVUrNUlpMWw3
|
||||
aHRFT1hBZTZHNHVtaGJCTEFyT2ZWNWsKLS0tIENxR3FKMEplV0V5Y0dsZzBkODVG
|
||||
U0ZlT2R0VFdEQmpsakVvZ1FJRkRQYTAKESQcSlMAuCG0/tLWWZaeoPayf1EKxSaf
|
||||
e4o8TmjmSgPEhepQ/LmNXl6obyuqUxztkh4Mm6pocqDp+gfzt7X/1g==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5WmthakxzRk5aeGJWZnRF
|
||||
bzdqM1o5Q1h2MFlpNEJvRUFZVU1wWjRaVWowClVKYkp0YUJCbUM0MWduaCtBSExH
|
||||
MDRWUGZkYTdndTZTeEM3ckxrMURFMUEKLS0tIHF3YUlLNndSWmxGejFPbTVjQnRS
|
||||
THF4RFBNbDJCdjVKRDJ6cEJlZGVBM0UKEDz1sKDON+lelq1zbCO4CETaK77T4G8y
|
||||
fly66sR5uIvAXdwXy14L9ssZsM1jOHqdkXnpbo6mzh3toB/ROzoL7w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-10-27T21:47:38Z"
|
||||
mac: ENC[AES256_GCM,data:bgZD+/LAPKp6DXyRMnbFWyxmiRjWxYO0ofxeGpO7nyw1DMSTD9s8li43eh/IP7wW4NORwzfIfzxUvOFmVfAiQKfkzpYRDFIKqK+aa/+43zppwpVT0UJ3CcBt2qgdJRHKnv9qeUOx4M8ulY7Xcsoevw8EGdrXCc1KAcDIwoIAp0Q=,iv:djYQ6YnGPrYGRKgpfbdf0beW6QHYTq5CJ3nes7+C4p4=,tag:N9n4VKyMNzGCmuYpwBLPng==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-06T02:01:24Z"
|
||||
mac: ENC[AES256_GCM,data:AI/znHiUa/Rwg0YRz65qUZ+b6V73oi3ZfAG/V5uZZvrGFrA0EISzD5LaP5Kl4GUU1svo4G5EI4aftPDgpdPptz21CCa8iYkXGe9pTDmtaE5ZICQAUbRcVhGMTfuWFhrgEhMfCJdOpfHaIQVEsQJQJgrnkXQVuUe9aT75LQz7pnY=,iv:KbIaYT0VeBT2WeSU5V5ZB4e+04JICYaY+egGN1Pv5bM=,tag:pPbhDxeLUx6V1XC/LPwxTg==,type:str]
|
||||
version: 3.13.2
|
||||
version: 3.9.3
|
||||
|
||||
@@ -15,8 +15,6 @@ files:
|
||||
- forgejo-registry-pull.enc.yaml
|
||||
- forgejo-runner-token.enc.yaml
|
||||
- forgejo-secrets.enc.yaml
|
||||
- comfyui-oauth2-proxy.enc.yaml
|
||||
- grafana-oidc-secrets.enc.yaml
|
||||
- grafana-secrets.enc.yaml
|
||||
- homelab-ca-secrets.enc.yaml
|
||||
- loki-secrets.enc.yaml
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rotate the OIDC client secret of every owned application, one at a time.
|
||||
|
||||
This is the manual execution of what a KeyRotation object declares. It exists so
|
||||
the credentials can rotate today, before the controller image is built, and so the
|
||||
controller has a reference implementation to match.
|
||||
|
||||
For each application: generate a new secret in ZITADEL, write it into the
|
||||
Kubernetes Secret, roll the consumer, then prove the consumer still talks to
|
||||
ZITADEL. A failure stops the run, because the next application should not start
|
||||
while one is in an unknown state.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location("zp", Path(__file__).with_name("zitadel-provision.py"))
|
||||
zp = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(zp)
|
||||
|
||||
PROJECT = "396757796898471937"
|
||||
ISSUER = "https://zitadel.riotpiao.com"
|
||||
|
||||
# app key, ZITADEL app id, Secret name, namespace, Secret key, consumer kind, consumer name
|
||||
APPS = [
|
||||
("grafana", "grafana", "grafana-oidc", "logging", "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET", "deploy", "grafana"),
|
||||
("argocd", "argocd", "oidc-secret", "argocd", "client-secret", "deploy", "argocd-server"),
|
||||
("comfyui", "comfyui", "comfyui-oauth2-proxy", "comfyui", "client-secret", "deploy", "comfyui-oauth2-proxy"),
|
||||
("minio", "minio", "minio-oidc", "storage", "MINIO_IDENTITY_OPENID_CLIENT_SECRET", "sts", "minio-cluster-az-a"),
|
||||
("paperless", "paperless", "paperless-oidc", "paperless", "client-secret", "deploy", "paperless"),
|
||||
("immich", "immich", "immich-oidc", "immich", "client-secret", "deploy", "immich-server"),
|
||||
("forgejo", "forgejo", "forgejo-oidc", "cicd", "CLIENT_SECRET", "deploy", "forgejo-gitea"),
|
||||
]
|
||||
|
||||
|
||||
def sh(*args: str, timeout: int = 120) -> tuple[int, str]:
|
||||
p = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
||||
return p.returncode, (p.stdout + p.stderr).strip()
|
||||
|
||||
|
||||
def state() -> dict:
|
||||
return json.load(open(Path.home() / ".config/zitadel/state.json"))
|
||||
|
||||
|
||||
def app_ids(token: str) -> dict[str, str]:
|
||||
"""Map clientId to the app resource id. The two are different, and the secret
|
||||
endpoint takes the resource id."""
|
||||
code, body = zp._api("POST", f"/management/v1/projects/{PROJECT}/apps/_search", token,
|
||||
{"query": {"limit": 100, "offset": 0}})
|
||||
out = {}
|
||||
for app in body.get("result", []) or []:
|
||||
cid = (app.get("oidcConfig") or {}).get("clientId")
|
||||
if cid and app.get("id"):
|
||||
out[cid] = app["id"]
|
||||
return out
|
||||
|
||||
|
||||
def new_secret(token: str, app_id: str) -> str | None:
|
||||
code, body = zp._api("POST", f"/management/v1/projects/{PROJECT}/apps/{app_id}/oidc_config/_generate_client_secret", token)
|
||||
if code != 200:
|
||||
print(f" generate failed: {code} {str(body)[:120]}")
|
||||
return None
|
||||
return body.get("clientSecret")
|
||||
|
||||
|
||||
def patch_secret(ns: str, name: str, key: str, value: str) -> bool:
|
||||
import base64
|
||||
enc = base64.b64encode(value.encode()).decode()
|
||||
patch = json.dumps({"data": {key: enc}})
|
||||
code, out = sh("kubectl", "-n", ns, "patch", "secret", name, "--type=merge", "-p", patch)
|
||||
if code != 0:
|
||||
print(f" secret patch failed: {out[:120]}")
|
||||
return code == 0
|
||||
|
||||
|
||||
def roll(kind: str, ns: str, name: str) -> bool:
|
||||
code, out = sh("kubectl", "-n", ns, "rollout", "restart", f"{kind}/{name}", timeout=120)
|
||||
if code != 0:
|
||||
print(f" rollout restart failed: {out[:120]}")
|
||||
return False
|
||||
code, out = sh("kubectl", "-n", ns, "rollout", "status", f"{kind}/{name}", "--timeout=90s", timeout=120)
|
||||
return code == 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
only = sys.argv[1:] or None
|
||||
token = zp.bootstrap_token()
|
||||
if not token:
|
||||
print("no bootstrap token")
|
||||
return 2
|
||||
cids = state()["client_ids"]["value"]
|
||||
ids = app_ids(token)
|
||||
print(f"resolved {len(ids)} app resource ids")
|
||||
done, failed = [], []
|
||||
for key, app_id, sname, ns, skey, kind, cname in APPS:
|
||||
if only and key not in only:
|
||||
continue
|
||||
print(f"[{key}] issuer={ISSUER} client_id={cids[key]}")
|
||||
rid = ids.get(cids[key])
|
||||
if not rid:
|
||||
print(f"[{key}] no app resource id for client {cids[key]}")
|
||||
failed.append(key)
|
||||
continue
|
||||
secret = new_secret(token, rid)
|
||||
if not secret:
|
||||
failed.append(key)
|
||||
continue
|
||||
if not patch_secret(ns, sname, skey, secret):
|
||||
failed.append(key)
|
||||
continue
|
||||
if not roll(kind, ns, cname):
|
||||
failed.append(key)
|
||||
print(f"[{key}] FAILED: consumer did not become ready")
|
||||
continue
|
||||
code, out = sh("kubectl", "-n", ns, "get", "pods", "-l",
|
||||
f"app={cname}" if kind == "deploy" else f"app.kubernetes.io/name={cname}",
|
||||
"--no-headers")
|
||||
ready = "Running" in out
|
||||
print(f"[{key}] rotated, consumer {'ready' if ready else 'NOT ready'}")
|
||||
(done if ready else failed).append(key)
|
||||
print(f"\nrotated: {len(done)} {done}\nfailed: {len(failed)} {failed}")
|
||||
return 0 if not failed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -143,8 +143,13 @@ OIDC_APPS = {
|
||||
"https://paperless.riotpiao.com/accounts/oidc/zitadel/login/callback/",
|
||||
"http://localhost:8000/accounts/oidc/authentik/login/callback/",
|
||||
"https://paperless.riotpiao.com/accounts/oidc/authentik/login/callback/"]},
|
||||
# The frontend calls back at /auth/callback (see OAUTH_CALLBACK_PATH in
|
||||
# poimen-frontend/lib/oauth.ts). The bare /callback spelling stays registered so
|
||||
# an older image still completes its login.
|
||||
"poimen": {**_WEB, "redirect_uris": [
|
||||
"http://localhost:3000/callback", "https://poimen.riotpiao.com/callback"]},
|
||||
"http://localhost:3000/callback", "https://poimen.riotpiao.com/callback",
|
||||
"http://localhost:3000/auth/callback",
|
||||
"https://poimen.riotpiao.com/auth/callback"]},
|
||||
"queue-sqs": {**_WEB, "redirect_uris": [
|
||||
"http://localhost:8080/callback", "https://queue.riotpiao.com/callback"]},
|
||||
"vault": {**_WEB, "redirect_uris": [
|
||||
|
||||
Reference in New Issue
Block a user