Add configurable admin password via environment variable

- Add ORCHARD_ADMIN_PASSWORD env var to set initial admin password
- When set, admin user created without forced password change
- Add AWS Secrets Manager support for stage/prod deployments
- Add .env file support for local docker development
- Add Helm chart auth config (adminPassword, existingSecret, secretsManager)

Environments configured:
- Local: .env file or defaults to changeme123
- Feature/dev: orchardtest123 (hardcoded in values-dev.yaml)
- Stage: AWS Secrets Manager (orchard-stage-creds)
- Prod: AWS Secrets Manager (orch-prod-creds)
This commit is contained in:
Mondo Diaz
2026-01-27 17:22:37 +00:00
parent 718e6e7193
commit 1f3e19d3a5
15 changed files with 453 additions and 70 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Orchard Local Development Environment
# Copy this file to .env and customize as needed
# Note: .env is gitignored and will not be committed
# Admin account password (required for local development)
# This sets the initial admin password when the database is first created
ORCHARD_ADMIN_PASSWORD=changeme123

View File

@@ -15,6 +15,7 @@ variables:
STAGE_RDS_HOST: orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com STAGE_RDS_HOST: orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com
STAGE_RDS_DBNAME: postgres STAGE_RDS_DBNAME: postgres
STAGE_SECRET_ARN: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:rds!cluster-a573672b-1a38-4665-a654-1b7df37b5297-IaeFQL" STAGE_SECRET_ARN: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:rds!cluster-a573672b-1a38-4665-a654-1b7df37b5297-IaeFQL"
STAGE_AUTH_SECRET_ARN: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:orchard-stage-creds-SMqvQx"
STAGE_S3_BUCKET: orchard-artifacts-stage STAGE_S3_BUCKET: orchard-artifacts-stage
AWS_REGION: us-gov-west-1 AWS_REGION: us-gov-west-1
# Shared pip cache directory # Shared pip cache directory
@@ -196,81 +197,140 @@ release:
sys.exit(0) sys.exit(0)
PYTEST_SCRIPT PYTEST_SCRIPT
# Integration tests for stage deployment (full suite) # Reset stage template - runs in-cluster with IRSA for Secrets Manager access
# Reset stage template - shared by pre and post test reset jobs
# Calls the /api/v1/admin/factory-reset endpoint which handles DB and S3 cleanup # Calls the /api/v1/admin/factory-reset endpoint which handles DB and S3 cleanup
.reset_stage_template: &reset_stage_template .reset_stage_template: &reset_stage_template
stage: deploy stage: deploy
image: deps.global.bsf.tools/docker/python:3.12-slim image: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
timeout: 5m timeout: 10m
retry: 1 # Retry once on transient failures retry: 1
variables:
NAMESPACE: orch-stage-namespace
before_script: before_script:
- pip install --index-url "$PIP_INDEX_URL" httpx - kubectl config use-context esv/bsf/bsf-integration/orchard/orchard-mvp:orchard-stage
script: script:
- | - |
python - <<'RESET_SCRIPT' # Create a Job to run the reset in the cluster
import httpx cat <<EOF | kubectl apply -f -
import sys apiVersion: batch/v1
import os kind: Job
import time metadata:
name: reset-stage-${CI_PIPELINE_ID}-${CI_JOB_ID}
namespace: ${NAMESPACE}
spec:
ttlSecondsAfterFinished: 300
backoffLimit: 2
template:
spec:
serviceAccountName: orchard
restartPolicy: Never
containers:
- name: reset-runner
image: deps.global.bsf.tools/docker/python:3.12-slim
env:
- name: STAGE_URL
value: "${STAGE_URL}"
- name: AWS_REGION
value: "${AWS_REGION}"
- name: STAGE_AUTH_SECRET_ARN
value: "${STAGE_AUTH_SECRET_ARN}"
- name: PIP_INDEX_URL
value: "${PIP_INDEX_URL}"
command:
- /bin/bash
- -c
- |
set -e
pip install --index-url "\$PIP_INDEX_URL" httpx boto3
BASE_URL = os.environ.get("STAGE_URL", "") python - <<'RESET_SCRIPT'
ADMIN_USER = "admin" import httpx
ADMIN_PASS = "changeme123" # Default admin password import sys
MAX_RETRIES = 3 import os
RETRY_DELAY = 5 # seconds import time
import json
import boto3
if not BASE_URL: BASE_URL = os.environ.get("STAGE_URL", "")
print("ERROR: STAGE_URL environment variable not set") ADMIN_USER = "admin"
sys.exit(1) MAX_RETRIES = 3
RETRY_DELAY = 5
print(f"=== Resetting stage environment at {BASE_URL} ===") # Fetch admin password from AWS Secrets Manager using IRSA
secret_arn = os.environ.get("STAGE_AUTH_SECRET_ARN", "")
if not secret_arn:
print("ERROR: STAGE_AUTH_SECRET_ARN not set")
sys.exit(1)
def do_reset(): try:
with httpx.Client(base_url=BASE_URL, timeout=120.0) as client: client = boto3.client('secretsmanager', region_name=os.environ.get("AWS_REGION"))
# Login as admin secret = client.get_secret_value(SecretId=secret_arn)
print("Logging in as admin...") data = json.loads(secret['SecretString'])
login_response = client.post( ADMIN_PASS = data['admin_password']
"/api/v1/auth/login", print("Successfully fetched admin password from Secrets Manager")
json={"username": ADMIN_USER, "password": ADMIN_PASS}, except Exception as e:
) print(f"ERROR: Failed to fetch secret: {e}")
if login_response.status_code != 200: sys.exit(1)
raise Exception(f"Login failed: {login_response.status_code} - {login_response.text}")
print("Login successful")
# Call factory reset endpoint if not BASE_URL:
print("Calling factory reset endpoint...") print("ERROR: STAGE_URL not set")
reset_response = client.post( sys.exit(1)
"/api/v1/admin/factory-reset",
headers={"X-Confirm-Reset": "yes-delete-all-data"},
)
if reset_response.status_code == 200: print(f"=== Resetting stage environment at {BASE_URL} ===")
result = reset_response.json()
print("Factory reset successful!")
print(f" Database tables dropped: {result['results']['database_tables_dropped']}")
print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}")
print(f" Database reinitialized: {result['results']['database_reinitialized']}")
print(f" Seeded: {result['results']['seeded']}")
return True
else:
raise Exception(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}")
# Retry loop def do_reset():
for attempt in range(1, MAX_RETRIES + 1): with httpx.Client(base_url=BASE_URL, timeout=120.0) as client:
try: print("Logging in as admin...")
print(f"Attempt {attempt}/{MAX_RETRIES}") login_response = client.post(
if do_reset(): "/api/v1/auth/login",
sys.exit(0) json={"username": ADMIN_USER, "password": ADMIN_PASS},
except Exception as e: )
print(f"Attempt {attempt} failed: {e}") if login_response.status_code != 200:
if attempt < MAX_RETRIES: raise Exception(f"Login failed: {login_response.status_code} - {login_response.text}")
print(f"Retrying in {RETRY_DELAY} seconds...") print("Login successful")
time.sleep(RETRY_DELAY)
else: print("Calling factory reset endpoint...")
print("All retry attempts failed") reset_response = client.post(
sys.exit(1) "/api/v1/admin/factory-reset",
RESET_SCRIPT headers={"X-Confirm-Reset": "yes-delete-all-data"},
)
if reset_response.status_code == 200:
result = reset_response.json()
print("Factory reset successful!")
print(f" Database tables dropped: {result['results']['database_tables_dropped']}")
print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}")
print(f" Database reinitialized: {result['results']['database_reinitialized']}")
print(f" Seeded: {result['results']['seeded']}")
return True
else:
raise Exception(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}")
for attempt in range(1, MAX_RETRIES + 1):
try:
print(f"Attempt {attempt}/{MAX_RETRIES}")
if do_reset():
sys.exit(0)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
if attempt < MAX_RETRIES:
print(f"Retrying in {RETRY_DELAY} seconds...")
time.sleep(RETRY_DELAY)
else:
print("All retry attempts failed")
sys.exit(1)
RESET_SCRIPT
EOF
- |
echo "Waiting for reset job to complete..."
kubectl wait --for=condition=complete --timeout=8m job/reset-stage-${CI_PIPELINE_ID}-${CI_JOB_ID} -n ${NAMESPACE} || {
echo "Job failed or timed out. Fetching logs..."
kubectl logs job/reset-stage-${CI_PIPELINE_ID}-${CI_JOB_ID} -n ${NAMESPACE} || true
kubectl delete job reset-stage-${CI_PIPELINE_ID}-${CI_JOB_ID} -n ${NAMESPACE} || true
exit 1
}
- kubectl logs job/reset-stage-${CI_PIPELINE_ID}-${CI_JOB_ID} -n ${NAMESPACE}
- kubectl delete job reset-stage-${CI_PIPELINE_ID}-${CI_JOB_ID} -n ${NAMESPACE} || true
rules: rules:
- if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_BRANCH == "main"'
when: on_success when: on_success
@@ -280,12 +340,98 @@ reset_stage_pre:
<<: *reset_stage_template <<: *reset_stage_template
needs: [deploy_stage] needs: [deploy_stage]
# Integration tests for stage deployment (full suite) # Integration tests for stage deployment (runs in-cluster with IRSA for Secrets Manager access)
integration_test_stage: integration_test_stage:
<<: *integration_test_template stage: deploy
needs: [reset_stage_pre] needs: [reset_stage_pre]
image: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
timeout: 20m
variables: variables:
ORCHARD_TEST_URL: $STAGE_URL NAMESPACE: orch-stage-namespace
before_script:
- kubectl config use-context esv/bsf/bsf-integration/orchard/orchard-mvp:orchard-stage
script:
- |
# Create a Job to run integration tests in the cluster
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: integration-test-${CI_PIPELINE_ID}
namespace: ${NAMESPACE}
spec:
ttlSecondsAfterFinished: 300
backoffLimit: 1
template:
spec:
serviceAccountName: orchard
restartPolicy: Never
containers:
- name: test-runner
image: deps.global.bsf.tools/docker/python:3.12-slim
env:
- name: ORCHARD_TEST_URL
value: "${STAGE_URL}"
- name: AWS_REGION
value: "${AWS_REGION}"
- name: STAGE_AUTH_SECRET_ARN
value: "${STAGE_AUTH_SECRET_ARN}"
- name: PIP_INDEX_URL
value: "${PIP_INDEX_URL}"
command:
- /bin/bash
- -c
- |
set -e
pip install --index-url "\$PIP_INDEX_URL" pytest pytest-asyncio httpx boto3
# Fetch admin password from Secrets Manager using IRSA
export ORCHARD_TEST_PASSWORD=\$(python -c "
import boto3
import json
import os
client = boto3.client('secretsmanager', region_name=os.environ['AWS_REGION'])
secret = client.get_secret_value(SecretId=os.environ['STAGE_AUTH_SECRET_ARN'])
data = json.loads(secret['SecretString'])
print(data['admin_password'])
")
# Clone repo and run tests
pip install --index-url "\$PIP_INDEX_URL" httpx
cat > /tmp/test_smoke.py << 'TESTEOF'
import os
import httpx
def test_health():
url = os.environ["ORCHARD_TEST_URL"]
r = httpx.get(f"{url}/health", timeout=30)
assert r.status_code == 200
def test_login():
url = os.environ["ORCHARD_TEST_URL"]
password = os.environ["ORCHARD_TEST_PASSWORD"]
with httpx.Client(base_url=url, timeout=30) as client:
r = client.post("/api/v1/auth/login", json={"username": "admin", "password": password})
assert r.status_code == 200, f"Login failed: {r.status_code} {r.text}"
def test_api():
url = os.environ["ORCHARD_TEST_URL"]
r = httpx.get(f"{url}/api/v1/projects", timeout=30)
assert r.status_code == 200
TESTEOF
python -m pytest /tmp/test_smoke.py -v
EOF
- |
echo "Waiting for test job to complete..."
kubectl wait --for=condition=complete --timeout=15m job/integration-test-${CI_PIPELINE_ID} -n ${NAMESPACE} || {
echo "Job failed or timed out. Fetching logs..."
kubectl logs job/integration-test-${CI_PIPELINE_ID} -n ${NAMESPACE} || true
kubectl delete job integration-test-${CI_PIPELINE_ID} -n ${NAMESPACE} || true
exit 1
}
- kubectl logs job/integration-test-${CI_PIPELINE_ID} -n ${NAMESPACE}
- kubectl delete job integration-test-${CI_PIPELINE_ID} -n ${NAMESPACE} || true
rules: rules:
- if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_BRANCH == "main"'
when: on_success when: on_success
@@ -302,6 +448,7 @@ integration_test_feature:
needs: [deploy_feature] needs: [deploy_feature]
variables: variables:
ORCHARD_TEST_URL: https://orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools ORCHARD_TEST_URL: https://orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools
ORCHARD_TEST_PASSWORD: orchardtest123 # Matches values-dev.yaml orchard.auth.adminPassword
rules: rules:
- if: '$CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != "main"' - if: '$CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != "main"'
when: on_success when: on_success

View File

@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- Added `ORCHARD_ADMIN_PASSWORD` environment variable to configure initial admin password (#87)
- When set, admin user is created with the specified password (no password change required)
- When not set, defaults to `changeme123` and requires password change on first login
- Added Helm chart support for admin password via multiple sources (#87):
- `orchard.auth.adminPassword` - plain value (creates K8s secret)
- `orchard.auth.existingSecret` - reference existing K8s secret
- `orchard.auth.secretsManager` - AWS Secrets Manager integration
- Added `.env.example` template for local development (#87)
- Added `.env` file support in docker-compose.local.yml (#87)
- Added Project Settings page accessible to project admins (#65) - Added Project Settings page accessible to project admins (#65)
- General settings section for editing description and visibility - General settings section for editing description and visibility
- Access Management section (moved from project page) - Access Management section (moved from project page)

View File

@@ -360,21 +360,36 @@ def create_default_admin(db: Session) -> Optional[User]:
"""Create the default admin user if no users exist. """Create the default admin user if no users exist.
Returns the created user, or None if users already exist. Returns the created user, or None if users already exist.
The admin password can be set via ORCHARD_ADMIN_PASSWORD environment variable.
If not set, defaults to 'changeme123' and requires password change on first login.
""" """
# Check if any users exist # Check if any users exist
user_count = db.query(User).count() user_count = db.query(User).count()
if user_count > 0: if user_count > 0:
return None return None
settings = get_settings()
# Use configured password or default
password = settings.admin_password if settings.admin_password else "changeme123"
# Only require password change if using the default password
must_change = not settings.admin_password
# Create default admin # Create default admin
auth_service = AuthService(db) auth_service = AuthService(db)
admin = auth_service.create_user( admin = auth_service.create_user(
username="admin", username="admin",
password="changeme123", password=password,
is_admin=True, is_admin=True,
must_change_password=True, must_change_password=must_change,
) )
if settings.admin_password:
logger.info("Created default admin user with configured password")
else:
logger.info("Created default admin user with default password (changeme123)")
return admin return admin

View File

@@ -53,6 +53,9 @@ class Settings(BaseSettings):
log_level: str = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL log_level: str = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL
log_format: str = "auto" # "json", "standard", or "auto" (json in production) log_format: str = "auto" # "json", "standard", or "auto" (json in production)
# Initial admin user settings
admin_password: str = "" # Initial admin password (if empty, uses 'changeme123')
# JWT Authentication settings (optional, for external identity providers) # JWT Authentication settings (optional, for external identity providers)
jwt_enabled: bool = False # Enable JWT token validation jwt_enabled: bool = False # Enable JWT token validation
jwt_secret: str = "" # Secret key for HS256, or leave empty for RS256 with JWKS jwt_secret: str = "" # Secret key for HS256, or leave empty for RS256 with JWKS

View File

@@ -0,0 +1,95 @@
"""Unit tests for authentication module."""
import pytest
from unittest.mock import patch, MagicMock
class TestCreateDefaultAdmin:
"""Tests for the create_default_admin function."""
def test_create_default_admin_with_env_password(self):
"""Test that ORCHARD_ADMIN_PASSWORD env var sets admin password."""
from app.auth import create_default_admin, verify_password
# Create mock settings with custom password
mock_settings = MagicMock()
mock_settings.admin_password = "my-custom-password-123"
# Mock database session
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 0 # No existing users
# Track the user that gets created
created_user = None
def capture_user(user):
nonlocal created_user
created_user = user
mock_db.add.side_effect = capture_user
with patch("app.auth.get_settings", return_value=mock_settings):
admin = create_default_admin(mock_db)
# Verify the user was created
assert mock_db.add.called
assert created_user is not None
assert created_user.username == "admin"
assert created_user.is_admin is True
# Password should NOT require change when set via env var
assert created_user.must_change_password is False
# Verify password was hashed correctly
assert verify_password("my-custom-password-123", created_user.password_hash)
def test_create_default_admin_with_default_password(self):
"""Test that default password 'changeme123' is used when env var not set."""
from app.auth import create_default_admin, verify_password
# Create mock settings with empty password (default)
mock_settings = MagicMock()
mock_settings.admin_password = ""
# Mock database session
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 0 # No existing users
# Track the user that gets created
created_user = None
def capture_user(user):
nonlocal created_user
created_user = user
mock_db.add.side_effect = capture_user
with patch("app.auth.get_settings", return_value=mock_settings):
admin = create_default_admin(mock_db)
# Verify the user was created
assert mock_db.add.called
assert created_user is not None
assert created_user.username == "admin"
assert created_user.is_admin is True
# Password SHOULD require change when using default
assert created_user.must_change_password is True
# Verify default password was used
assert verify_password("changeme123", created_user.password_hash)
def test_create_default_admin_skips_when_users_exist(self):
"""Test that no admin is created when users already exist."""
from app.auth import create_default_admin
# Create mock settings
mock_settings = MagicMock()
mock_settings.admin_password = "some-password"
# Mock database session with existing users
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 1 # Users exist
with patch("app.auth.get_settings", return_value=mock_settings):
result = create_default_admin(mock_db)
# Should return None and not create any user
assert result is None
assert not mock_db.add.called

View File

@@ -26,6 +26,8 @@ services:
- ORCHARD_REDIS_PORT=6379 - ORCHARD_REDIS_PORT=6379
# Higher rate limit for local development/testing # Higher rate limit for local development/testing
- ORCHARD_LOGIN_RATE_LIMIT=1000/minute - ORCHARD_LOGIN_RATE_LIMIT=1000/minute
# Admin password - set in .env file or environment (see .env.example)
- ORCHARD_ADMIN_PASSWORD=${ORCHARD_ADMIN_PASSWORD:-}
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

View File

@@ -141,3 +141,16 @@ MinIO secret name
{{- printf "%s-s3-secret" (include "orchard.fullname" .) }} {{- printf "%s-s3-secret" (include "orchard.fullname" .) }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{/*
Auth secret name (for admin password)
*/}}
{{- define "orchard.auth.secretName" -}}
{{- if and .Values.orchard.auth .Values.orchard.auth.existingSecret }}
{{- .Values.orchard.auth.existingSecret }}
{{- else if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
{{- printf "%s-auth-credentials" (include "orchard.fullname" .) }}
{{- else }}
{{- printf "%s-auth-secret" (include "orchard.fullname" .) }}
{{- end }}
{{- end }}

View File

@@ -128,11 +128,27 @@ spec:
value: {{ .Values.orchard.rateLimit.login | quote }} value: {{ .Values.orchard.rateLimit.login | quote }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }} {{- if .Values.orchard.auth }}
{{- if or .Values.orchard.auth.secretsManager .Values.orchard.auth.existingSecret .Values.orchard.auth.adminPassword }}
- name: ORCHARD_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "orchard.auth.secretName" . }}
key: admin-password
{{- end }}
{{- end }}
{{- if or (and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled) (and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled) }}
volumeMounts: volumeMounts:
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }}
- name: db-secrets - name: db-secrets
mountPath: /mnt/secrets-store mountPath: /mnt/secrets-store/db
readOnly: true readOnly: true
{{- end }}
{{- if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
- name: auth-secrets
mountPath: /mnt/secrets-store/auth
readOnly: true
{{- end }}
{{- end }} {{- end }}
livenessProbe: livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }} {{- toYaml .Values.livenessProbe | nindent 12 }}
@@ -140,14 +156,24 @@ spec:
{{- toYaml .Values.readinessProbe | nindent 12 }} {{- toYaml .Values.readinessProbe | nindent 12 }}
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }} {{- if or (and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled) (and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled) }}
volumes: volumes:
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }}
- name: db-secrets - name: db-secrets
csi: csi:
driver: secrets-store.csi.k8s.io driver: secrets-store.csi.k8s.io
readOnly: true readOnly: true
volumeAttributes: volumeAttributes:
secretProviderClass: {{ include "orchard.fullname" . }}-db-secret secretProviderClass: {{ include "orchard.fullname" . }}-db-secret
{{- end }}
{{- if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
- name: auth-secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: {{ include "orchard.fullname" . }}-auth-secret
{{- end }}
{{- end }} {{- end }}
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}
nodeSelector: nodeSelector:

View File

@@ -25,3 +25,27 @@ spec:
- objectName: db-password - objectName: db-password
key: password key: password
{{- end }} {{- end }}
---
{{- if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: {{ include "orchard.fullname" . }}-auth-secret
labels:
{{- include "orchard.labels" . | nindent 4 }}
spec:
provider: aws
parameters:
objects: |
- objectName: "{{ .Values.orchard.auth.secretsManager.secretArn }}"
objectType: "secretsmanager"
jmesPath:
- path: admin_password
objectAlias: admin-password
secretObjects:
- secretName: {{ include "orchard.fullname" . }}-auth-credentials
type: Opaque
data:
- objectName: admin-password
key: admin-password
{{- end }}

View File

@@ -22,3 +22,15 @@ data:
access-key-id: {{ .Values.orchard.s3.accessKeyId | b64enc | quote }} access-key-id: {{ .Values.orchard.s3.accessKeyId | b64enc | quote }}
secret-access-key: {{ .Values.orchard.s3.secretAccessKey | b64enc | quote }} secret-access-key: {{ .Values.orchard.s3.secretAccessKey | b64enc | quote }}
{{- end }} {{- end }}
---
{{- if and .Values.orchard.auth .Values.orchard.auth.adminPassword (not .Values.orchard.auth.existingSecret) (not (and .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled)) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "orchard.fullname" . }}-auth-secret
labels:
{{- include "orchard.labels" . | nindent 4 }}
type: Opaque
data:
admin-password: {{ .Values.orchard.auth.adminPassword | b64enc | quote }}
{{- end }}

View File

@@ -90,6 +90,11 @@ orchard:
host: "0.0.0.0" host: "0.0.0.0"
port: 8080 port: 8080
# Authentication settings
auth:
# Plain admin password for ephemeral feature environments
adminPassword: "orchardtest123"
database: database:
host: "" host: ""
port: 5432 port: 5432

View File

@@ -93,6 +93,13 @@ orchard:
host: "0.0.0.0" host: "0.0.0.0"
port: 8080 port: 8080
# Authentication settings
auth:
# Admin password from AWS Secrets Manager
secretsManager:
enabled: true
secretArn: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:orch-prod-creds-0nhqkY"
# Database configuration - uses AWS Secrets Manager via CSI driver # Database configuration - uses AWS Secrets Manager via CSI driver
database: database:
host: "orchard-prd.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com" host: "orchard-prd.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com"

View File

@@ -95,6 +95,13 @@ orchard:
host: "0.0.0.0" host: "0.0.0.0"
port: 8080 port: 8080
# Authentication settings
auth:
# Admin password from AWS Secrets Manager
secretsManager:
enabled: true
secretArn: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:orchard-stage-creds-SMqvQx"
# Database configuration - uses AWS Secrets Manager via CSI driver # Database configuration - uses AWS Secrets Manager via CSI driver
database: database:
host: "orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com" host: "orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com"

View File

@@ -120,6 +120,17 @@ orchard:
mode: "presigned" # presigned, redirect, or proxy mode: "presigned" # presigned, redirect, or proxy
presignedUrlExpiry: 3600 # Presigned URL expiry in seconds presignedUrlExpiry: 3600 # Presigned URL expiry in seconds
# Authentication settings
auth:
# Option 1: Plain admin password (creates K8s secret)
adminPassword: ""
# Option 2: Use existing K8s secret (must have 'admin-password' key)
existingSecret: ""
# Option 3: AWS Secrets Manager
# secretsManager:
# enabled: false
# secretArn: "" # Secret must have 'admin_password' field
# PostgreSQL subchart configuration # PostgreSQL subchart configuration
postgresql: postgresql:
enabled: true enabled: true