Compare commits
8 Commits
5ac2f931f8
...
1daa6f6e90
| Author | SHA1 | Date | |
|---|---|---|---|
| 1daa6f6e90 | |||
| f28240c37f | |||
| 95fbe99b61 | |||
| ff4aa155d1 | |||
| 924e51ffaa | |||
| 58f0fd50d8 | |||
| 40113bc634 | |||
| 4c4cdf0a52 |
25
backend/alembic/versions/0002_keycloak_sso.py
Normal file
25
backend/alembic/versions/0002_keycloak_sso.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""add keycloak_sub, make hashed_password nullable
|
||||||
|
|
||||||
|
Revision ID: 0002
|
||||||
|
Revises: 0001
|
||||||
|
Create Date: 2026-03-01
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "0002"
|
||||||
|
down_revision = "0001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("users", sa.Column("keycloak_sub", sa.String(255), nullable=True))
|
||||||
|
op.create_unique_constraint("uq_users_keycloak_sub", "users", ["keycloak_sub"])
|
||||||
|
op.alter_column("users", "hashed_password", nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column("users", "hashed_password", nullable=False)
|
||||||
|
op.drop_constraint("uq_users_keycloak_sub", "users", type_="unique")
|
||||||
|
op.drop_column("users", "keycloak_sub")
|
||||||
@@ -1,43 +1,60 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from authlib.integrations.starlette_client import OAuthError
|
||||||
from app import models, schemas
|
from app import models, schemas
|
||||||
from app.auth import create_access_token, get_current_user, hash_password, verify_password
|
from app.auth import create_access_token, get_current_user, oauth
|
||||||
from app.database import get_db
|
from app.database import get_db, settings
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
FIREWALL_ADMINS_GROUP = "firewall admins"
|
||||||
|
|
||||||
@router.post("/register", response_model=schemas.UserOut, status_code=201)
|
|
||||||
def register(body: schemas.UserCreate, db: Session = Depends(get_db)) -> models.User:
|
@router.get("/oidc/login")
|
||||||
if db.query(models.User).filter(models.User.username == body.username).first():
|
async def oidc_login(request: Request) -> RedirectResponse:
|
||||||
raise HTTPException(status_code=400, detail="Username already registered")
|
return await oauth.keycloak.authorize_redirect(request, settings.keycloak_redirect_uri)
|
||||||
if db.query(models.User).filter(models.User.email == body.email).first():
|
|
||||||
raise HTTPException(status_code=400, detail="Email already registered")
|
|
||||||
|
@router.get("/oidc/callback")
|
||||||
|
async def oidc_callback(request: Request, db: Session = Depends(get_db)) -> RedirectResponse:
|
||||||
|
try:
|
||||||
|
token = await oauth.keycloak.authorize_access_token(request)
|
||||||
|
except OAuthError as e:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
|
|
||||||
|
userinfo = token.get("userinfo") or {}
|
||||||
|
groups = userinfo.get("groups", [])
|
||||||
|
if FIREWALL_ADMINS_GROUP not in groups:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not in firewall admins group")
|
||||||
|
|
||||||
|
sub = userinfo["sub"]
|
||||||
|
email = userinfo.get("email", "")
|
||||||
|
username = userinfo.get("preferred_username", sub)
|
||||||
|
|
||||||
|
user = db.query(models.User).filter(models.User.keycloak_sub == sub).first()
|
||||||
|
if not user:
|
||||||
user = models.User(
|
user = models.User(
|
||||||
username=body.username,
|
keycloak_sub=sub,
|
||||||
email=body.email,
|
email=email,
|
||||||
hashed_password=hash_password(body.password),
|
username=username,
|
||||||
|
hashed_password=None,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
return user
|
|
||||||
|
|
||||||
|
access_token = create_access_token(user.id)
|
||||||
@router.post("/login")
|
response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||||
def login(body: schemas.LoginRequest, response: Response, db: Session = Depends(get_db)) -> dict:
|
|
||||||
user = db.query(models.User).filter(models.User.username == body.username).first()
|
|
||||||
if not user or not verify_password(body.password, user.hashed_password):
|
|
||||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
||||||
token = create_access_token(user.id)
|
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="access_token",
|
key="access_token",
|
||||||
value=token,
|
value=access_token,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
samesite="lax",
|
samesite="lax",
|
||||||
max_age=3600,
|
max_age=3600,
|
||||||
)
|
)
|
||||||
return {"message": "Logged in"}
|
return response
|
||||||
|
|
||||||
|
|
||||||
@router.post("/logout")
|
@router.post("/logout")
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from passlib.context import CryptContext
|
from authlib.integrations.starlette_client import OAuth
|
||||||
from fastapi import Cookie, HTTPException, status, Depends
|
from fastapi import Cookie, HTTPException, status, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.database import get_db, settings
|
from app.database import get_db, settings
|
||||||
from app import models
|
from app import models
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
oauth = OAuth()
|
||||||
|
oauth.register(
|
||||||
|
name="keycloak",
|
||||||
def hash_password(password: str) -> str:
|
client_id=settings.keycloak_client_id,
|
||||||
return pwd_context.hash(password)
|
client_secret=settings.keycloak_client_secret,
|
||||||
|
server_metadata_url=(
|
||||||
|
f"{settings.keycloak_url}/realms/{settings.keycloak_realm}"
|
||||||
def verify_password(plain: str, hashed: str) -> bool:
|
"/.well-known/openid-configuration"
|
||||||
return pwd_context.verify(plain, hashed)
|
),
|
||||||
|
client_kwargs={"scope": "openid email profile"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(user_id: int) -> str:
|
def create_access_token(user_id: int) -> str:
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ class Settings(BaseSettings):
|
|||||||
jwt_secret_key: str
|
jwt_secret_key: str
|
||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
jwt_expire_minutes: int = 60
|
jwt_expire_minutes: int = 60
|
||||||
|
keycloak_url: str
|
||||||
|
keycloak_realm: str
|
||||||
|
keycloak_client_id: str
|
||||||
|
keycloak_client_secret: str
|
||||||
|
keycloak_redirect_uri: str
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from app.api import auth, configs, zones, interfaces, policies, rules, masq
|
from app.api import auth, configs, zones, interfaces, policies, rules, masq
|
||||||
|
from app.database import settings
|
||||||
|
|
||||||
app = FastAPI(title="Shorefront", version="0.1.0")
|
app = FastAPI(title="Shorefront", version="0.1.0")
|
||||||
|
|
||||||
|
app.add_middleware(SessionMiddleware, secret_key=settings.jwt_secret_key)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["http://localhost:5173", "http://localhost:80"],
|
allow_origins=["http://localhost:5173", "http://localhost:80"],
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class User(Base):
|
|||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
||||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
||||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
|
keycloak_sub: Mapped[str | None] = mapped_column(String(255), unique=True, nullable=True)
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
configs: Mapped[list["Config"]] = relationship("Config", back_populates="owner")
|
configs: Mapped[list["Config"]] = relationship("Config", back_populates="owner")
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
# --- Auth ---
|
# --- Auth ---
|
||||||
class UserCreate(BaseModel):
|
|
||||||
username: str
|
|
||||||
email: EmailStr
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
class UserOut(BaseModel):
|
class UserOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
username: str
|
username: str
|
||||||
@@ -19,11 +13,6 @@ class UserOut(BaseModel):
|
|||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
# --- Config ---
|
# --- Config ---
|
||||||
class ConfigCreate(BaseModel):
|
class ConfigCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ sqlalchemy==2.0.30
|
|||||||
alembic==1.13.1
|
alembic==1.13.1
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]==3.3.0
|
||||||
passlib[bcrypt]==1.7.4
|
|
||||||
bcrypt<4.0.0
|
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
pydantic[email]==2.7.1
|
pydantic[email]==2.7.1
|
||||||
pydantic-settings==2.2.1
|
pydantic-settings==2.2.1
|
||||||
|
authlib==1.3.1
|
||||||
|
httpx==0.27.0
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
|||||||
75
docs/plans/2026-03-01-keycloak-sso-design.md
Normal file
75
docs/plans/2026-03-01-keycloak-sso-design.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Keycloak SSO Integration Design
|
||||||
|
|
||||||
|
**Goal:** Replace local username/password auth with Keycloak OIDC using a backend callback flow.
|
||||||
|
|
||||||
|
**Architecture:** Backend acts as OIDC confidential client. Browser is redirected to Keycloak, which redirects back to a backend callback endpoint. Backend validates the token, checks group membership, provisions the user, and issues its own httpOnly JWT cookie. Frontend is unchanged except the login page.
|
||||||
|
|
||||||
|
**Tech Stack:** authlib (OIDC), FastAPI, Alembic, React
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser → GET /api/auth/oidc/login
|
||||||
|
→ backend generates state, stores in short-lived cookie, redirects to Keycloak
|
||||||
|
|
||||||
|
Keycloak → user authenticates → redirects to:
|
||||||
|
GET /api/auth/oidc/callback?code=...&state=...
|
||||||
|
|
||||||
|
Backend:
|
||||||
|
1. Validates state cookie (CSRF protection)
|
||||||
|
2. Exchanges code for tokens via Keycloak token endpoint
|
||||||
|
3. Validates ID token signature via Keycloak JWKS (authlib handles this)
|
||||||
|
4. Checks groups claim for "firewall admins" → 403 if absent
|
||||||
|
5. Looks up user by keycloak_sub → auto-provisions row if first login
|
||||||
|
6. Issues httpOnly JWT cookie (same mechanism as before)
|
||||||
|
7. Redirects browser to /
|
||||||
|
```
|
||||||
|
|
||||||
|
Removed endpoints: `POST /auth/login`, `POST /auth/register`
|
||||||
|
Kept endpoints: `GET /auth/me`, `POST /auth/logout`
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
New Alembic migration:
|
||||||
|
- Add `keycloak_sub VARCHAR(255) UNIQUE` to `users` table
|
||||||
|
- Make `hashed_password` nullable (always NULL for SSO users; kept for schema stability)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**ConfigMap** (non-secret):
|
||||||
|
- `KEYCLOAK_URL`: `https://sso.baumann.gr`
|
||||||
|
- `KEYCLOAK_REALM`: `homelab`
|
||||||
|
- `KEYCLOAK_CLIENT_ID`: `shorefront`
|
||||||
|
|
||||||
|
**Secret** (added to `scripts/create-secrets.sh`):
|
||||||
|
- `KEYCLOAK_CLIENT_SECRET`
|
||||||
|
|
||||||
|
**Redirect URI** (backend callback, registered in Keycloak):
|
||||||
|
- `https://shorefront.baumann.gr/api/auth/oidc/callback`
|
||||||
|
|
||||||
|
## Backend Changes
|
||||||
|
|
||||||
|
- Add `authlib` + `httpx` to `requirements.txt`
|
||||||
|
- Add `keycloak_url`, `keycloak_realm`, `keycloak_client_id`, `keycloak_client_secret` to `Settings`
|
||||||
|
- Add `keycloak_sub` column to `User` model
|
||||||
|
- New migration: add `keycloak_sub`, make `hashed_password` nullable
|
||||||
|
- Replace `backend/app/api/auth.py` with OIDC endpoints:
|
||||||
|
- `GET /auth/oidc/login` — generate state, redirect to Keycloak
|
||||||
|
- `GET /auth/oidc/callback` — exchange code, validate token, check group, provision user, set cookie, redirect
|
||||||
|
- Keep `POST /auth/logout`, `GET /auth/me`
|
||||||
|
- Remove `hash_password`, `verify_password` from `auth.py`
|
||||||
|
|
||||||
|
## Frontend Changes
|
||||||
|
|
||||||
|
- `Login.tsx`: replace username/password form with a single "Sign in with SSO" button (`window.location.href = '/api/auth/oidc/login'`)
|
||||||
|
- All other components unchanged
|
||||||
|
|
||||||
|
## Keycloak Manual Setup (pre-deploy)
|
||||||
|
|
||||||
|
1. Create client `shorefront`, access type: confidential
|
||||||
|
2. Set Valid Redirect URIs: `https://shorefront.baumann.gr/api/auth/oidc/callback`
|
||||||
|
3. Set Web Origins: `https://shorefront.baumann.gr`
|
||||||
|
4. Add Group Membership mapper on client: include groups in ID token, claim name `groups`
|
||||||
|
5. Create group `firewall admins`, add users to it
|
||||||
@@ -33,6 +33,31 @@ spec:
|
|||||||
key: JWT_SECRET_KEY
|
key: JWT_SECRET_KEY
|
||||||
- name: DATABASE_URL
|
- name: DATABASE_URL
|
||||||
value: "postgresql://{{ .Values.postgres.user }}:$(POSTGRES_PASSWORD)@postgres:5432/{{ .Values.postgres.database }}"
|
value: "postgresql://{{ .Values.postgres.user }}:$(POSTGRES_PASSWORD)@postgres:5432/{{ .Values.postgres.database }}"
|
||||||
|
- name: KEYCLOAK_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_URL
|
||||||
|
- name: KEYCLOAK_REALM
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_REALM
|
||||||
|
- name: KEYCLOAK_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_CLIENT_ID
|
||||||
|
- name: KEYCLOAK_REDIRECT_URI
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_REDIRECT_URI
|
||||||
|
- name: KEYCLOAK_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: shorefront-secret
|
||||||
|
key: KEYCLOAK_CLIENT_SECRET
|
||||||
containers:
|
containers:
|
||||||
- name: backend
|
- name: backend
|
||||||
image: "{{ .Values.backend.image }}:{{ .Values.containers.version }}"
|
image: "{{ .Values.backend.image }}:{{ .Values.containers.version }}"
|
||||||
@@ -60,6 +85,31 @@ spec:
|
|||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: shorefront-config
|
name: shorefront-config
|
||||||
key: JWT_EXPIRE_MINUTES
|
key: JWT_EXPIRE_MINUTES
|
||||||
|
- name: KEYCLOAK_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_URL
|
||||||
|
- name: KEYCLOAK_REALM
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_REALM
|
||||||
|
- name: KEYCLOAK_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_CLIENT_ID
|
||||||
|
- name: KEYCLOAK_REDIRECT_URI
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: shorefront-config
|
||||||
|
key: KEYCLOAK_REDIRECT_URI
|
||||||
|
- name: KEYCLOAK_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: shorefront-secret
|
||||||
|
key: KEYCLOAK_CLIENT_SECRET
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ data:
|
|||||||
POSTGRES_USER: {{ .Values.postgres.user | quote }}
|
POSTGRES_USER: {{ .Values.postgres.user | quote }}
|
||||||
JWT_ALGORITHM: "HS256"
|
JWT_ALGORITHM: "HS256"
|
||||||
JWT_EXPIRE_MINUTES: "60"
|
JWT_EXPIRE_MINUTES: "60"
|
||||||
|
KEYCLOAK_URL: {{ .Values.keycloak.url | quote }}
|
||||||
|
KEYCLOAK_REALM: {{ .Values.keycloak.realm | quote }}
|
||||||
|
KEYCLOAK_CLIENT_ID: {{ .Values.keycloak.clientId | quote }}
|
||||||
|
KEYCLOAK_REDIRECT_URI: {{ .Values.keycloak.redirectUri | quote }}
|
||||||
|
|||||||
@@ -13,13 +13,6 @@ spec:
|
|||||||
- host: {{ .Values.ingress.host }}
|
- host: {{ .Values.ingress.host }}
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /api
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: backend
|
|
||||||
port:
|
|
||||||
number: 8000
|
|
||||||
- path: /
|
- path: /
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
|
|||||||
@@ -35,5 +35,11 @@ ingress:
|
|||||||
host: shorefront.baumann.gr
|
host: shorefront.baumann.gr
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
url: https://sso.baumann.gr
|
||||||
|
realm: homelab
|
||||||
|
clientId: shorefront
|
||||||
|
redirectUri: https://shorefront.baumann.gr/api/auth/oidc/callback
|
||||||
|
|
||||||
containers:
|
containers:
|
||||||
version: "0.002"
|
version: "0.002"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ fi
|
|||||||
# --- Validate required env vars ---
|
# --- Validate required env vars ---
|
||||||
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}"
|
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}"
|
||||||
: "${JWT_SECRET_KEY:?JWT_SECRET_KEY is required}"
|
: "${JWT_SECRET_KEY:?JWT_SECRET_KEY is required}"
|
||||||
|
: "${KEYCLOAK_CLIENT_SECRET:?KEYCLOAK_CLIENT_SECRET is required}"
|
||||||
|
|
||||||
echo "Creating namespace '${NAMESPACE}' if it does not exist..."
|
echo "Creating namespace '${NAMESPACE}' if it does not exist..."
|
||||||
kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
|
kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
|
||||||
@@ -21,6 +22,7 @@ kubectl create secret generic shorefront-secret \
|
|||||||
--namespace "${NAMESPACE}" \
|
--namespace "${NAMESPACE}" \
|
||||||
--from-literal="POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" \
|
--from-literal="POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" \
|
||||||
--from-literal="JWT_SECRET_KEY=${JWT_SECRET_KEY}" \
|
--from-literal="JWT_SECRET_KEY=${JWT_SECRET_KEY}" \
|
||||||
|
--from-literal="KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET}" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
echo "Done. Secret 'shorefront-secret' is ready in namespace '${NAMESPACE}'."
|
echo "Done. Secret 'shorefront-secret' is ready in namespace '${NAMESPACE}'."
|
||||||
|
|||||||
Reference in New Issue
Block a user