From 7120cf64f13676848ce95996a9d0ba657a7f4d36 Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Tue, 27 Jan 2026 14:23:40 -0600 Subject: [PATCH] Add configurable admin password via environment variable --- .env.example | 7 ++ .gitlab-ci.yml | 29 ++++-- CHANGELOG.md | 9 ++ backend/app/auth.py | 19 +++- backend/app/config.py | 3 + backend/tests/conftest.py | 20 ++++ backend/tests/integration/test_auth_api.py | 56 +++++------ backend/tests/unit/test_auth.py | 95 +++++++++++++++++++ docker-compose.local.yml | 2 + helm/orchard/templates/_helpers.tpl | 13 +++ helm/orchard/templates/deployment.yaml | 32 ++++++- .../templates/secret-provider-class.yaml | 24 +++++ helm/orchard/templates/secret.yaml | 12 +++ helm/orchard/values-dev.yaml | 4 + helm/orchard/values-prod.yaml | 7 ++ helm/orchard/values-stage.yaml | 7 ++ helm/orchard/values.yaml | 11 +++ 17 files changed, 308 insertions(+), 42 deletions(-) create mode 100644 .env.example create mode 100644 backend/tests/unit/test_auth.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dbee420 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5705dfb..20c0cac 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,7 @@ variables: STAGE_RDS_HOST: orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com 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_AUTH_SECRET_ARN: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:orchard-stage-creds-SMqvQx" STAGE_S3_BUCKET: orchard-artifacts-stage AWS_REGION: us-gov-west-1 # Shared pip cache directory @@ -117,6 +118,9 @@ release: - pip install --index-url "$PIP_INDEX_URL" pytest pytest-asyncio httpx script: - cd backend + # Debug: Print environment variables for test configuration + - echo "ORCHARD_TEST_URL=$ORCHARD_TEST_URL" + - echo "ORCHARD_TEST_PASSWORD is set to '${ORCHARD_TEST_PASSWORD:-NOT SET}'" # Run full integration test suite, excluding: # - large/slow tests # - requires_direct_s3 tests (can't access MinIO from outside K8s cluster) @@ -196,14 +200,13 @@ release: sys.exit(0) PYTEST_SCRIPT -# Integration tests for stage deployment (full suite) -# Reset stage template - shared by pre and post test reset jobs +# Reset stage template - runs from CI runner, uses CI variable for auth # Calls the /api/v1/admin/factory-reset endpoint which handles DB and S3 cleanup .reset_stage_template: &reset_stage_template stage: deploy image: deps.global.bsf.tools/docker/python:3.12-slim timeout: 5m - retry: 1 # Retry once on transient failures + retry: 1 before_script: - pip install --index-url "$PIP_INDEX_URL" httpx script: @@ -216,19 +219,22 @@ release: BASE_URL = os.environ.get("STAGE_URL", "") ADMIN_USER = "admin" - ADMIN_PASS = "changeme123" # Default admin password + ADMIN_PASS = os.environ.get("STAGE_ADMIN_PASSWORD", "") MAX_RETRIES = 3 - RETRY_DELAY = 5 # seconds + RETRY_DELAY = 5 if not BASE_URL: - print("ERROR: STAGE_URL environment variable not set") + print("ERROR: STAGE_URL not set") + sys.exit(1) + + if not ADMIN_PASS: + print("ERROR: STAGE_ADMIN_PASSWORD not set") sys.exit(1) print(f"=== Resetting stage environment at {BASE_URL} ===") def do_reset(): with httpx.Client(base_url=BASE_URL, timeout=120.0) as client: - # Login as admin print("Logging in as admin...") login_response = client.post( "/api/v1/auth/login", @@ -238,7 +244,6 @@ release: raise Exception(f"Login failed: {login_response.status_code} - {login_response.text}") print("Login successful") - # Call factory reset endpoint print("Calling factory reset endpoint...") reset_response = client.post( "/api/v1/admin/factory-reset", @@ -256,7 +261,6 @@ release: else: raise Exception(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}") - # Retry loop for attempt in range(1, MAX_RETRIES + 1): try: print(f"Attempt {attempt}/{MAX_RETRIES}") @@ -280,12 +284,14 @@ reset_stage_pre: <<: *reset_stage_template needs: [deploy_stage] -# Integration tests for stage deployment (full suite) +# Integration tests for stage deployment +# Uses CI variable STAGE_ADMIN_PASSWORD (set in GitLab CI/CD settings) integration_test_stage: <<: *integration_test_template needs: [reset_stage_pre] variables: ORCHARD_TEST_URL: $STAGE_URL + ORCHARD_TEST_PASSWORD: $STAGE_ADMIN_PASSWORD rules: - if: '$CI_COMMIT_BRANCH == "main"' when: on_success @@ -297,11 +303,13 @@ reset_stage: allow_failure: true # Don't fail pipeline if reset has issues # Integration tests for feature deployment (full suite) +# Uses DEV_ADMIN_PASSWORD CI variable (same as deploy_feature) integration_test_feature: <<: *integration_test_template needs: [deploy_feature] variables: ORCHARD_TEST_URL: https://orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools + ORCHARD_TEST_PASSWORD: $DEV_ADMIN_PASSWORD rules: - if: '$CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != "main"' when: on_success @@ -453,6 +461,7 @@ deploy_feature: --namespace $NAMESPACE \ -f $VALUES_FILE \ --set image.tag=git.linux-amd64-$CI_COMMIT_SHA \ + --set orchard.auth.adminPassword=$DEV_ADMIN_PASSWORD \ --set ingress.hosts[0].host=orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools \ --set ingress.tls[0].hosts[0]=orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools \ --set ingress.tls[0].secretName=orchard-$CI_COMMIT_REF_SLUG-tls \ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3f4d9..6f8eaa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### 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) - General settings section for editing description and visibility - Access Management section (moved from project page) diff --git a/backend/app/auth.py b/backend/app/auth.py index c35ca38..1cf41cf 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -360,21 +360,36 @@ def create_default_admin(db: Session) -> Optional[User]: """Create the default admin user if no users 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 user_count = db.query(User).count() if user_count > 0: 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 auth_service = AuthService(db) admin = auth_service.create_user( username="admin", - password="changeme123", + password=password, 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 diff --git a/backend/app/config.py b/backend/app/config.py index 8a19d89..a691767 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -53,6 +53,9 @@ class Settings(BaseSettings): log_level: str = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL 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_enabled: bool = False # Enable JWT token validation jwt_secret: str = "" # Secret key for HS256, or leave empty for RS256 with JWKS diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 71fb905..fb11398 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -56,6 +56,26 @@ os.environ.setdefault("ORCHARD_S3_BUCKET", "test-bucket") os.environ.setdefault("ORCHARD_S3_ACCESS_KEY_ID", "test") os.environ.setdefault("ORCHARD_S3_SECRET_ACCESS_KEY", "test") + +# ============================================================================= +# Admin Credentials Helper +# ============================================================================= + + +def get_admin_password() -> str: + """Get the admin password for test authentication. + + Returns the password from ORCHARD_TEST_PASSWORD environment variable, + or 'changeme123' as the default for local development. + """ + return os.environ.get("ORCHARD_TEST_PASSWORD", "changeme123") + + +def get_admin_username() -> str: + """Get the admin username for test authentication.""" + return os.environ.get("ORCHARD_TEST_USERNAME", "admin") + + # Re-export factory functions for backward compatibility from tests.factories import ( create_test_file, diff --git a/backend/tests/integration/test_auth_api.py b/backend/tests/integration/test_auth_api.py index 13e5259..7b65bf5 100644 --- a/backend/tests/integration/test_auth_api.py +++ b/backend/tests/integration/test_auth_api.py @@ -8,6 +8,8 @@ allow these tests to run. Production uses strict rate limits (5/minute). import pytest from uuid import uuid4 +from tests.conftest import get_admin_password, get_admin_username + # Mark all tests in this module as auth_intensive (informational, not excluded from CI) pytestmark = pytest.mark.auth_intensive @@ -21,11 +23,11 @@ class TestAuthLogin: """Test successful login with default admin credentials.""" response = auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) assert response.status_code == 200 data = response.json() - assert data["username"] == "admin" + assert data["username"] == get_admin_username() assert data["is_admin"] is True assert "orchard_session" in response.cookies @@ -34,7 +36,7 @@ class TestAuthLogin: """Test login with wrong password.""" response = auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "wrongpassword"}, + json={"username": get_admin_username(), "password": "wrongpassword"}, ) assert response.status_code == 401 assert "Invalid username or password" in response.json()["detail"] @@ -58,7 +60,7 @@ class TestAuthLogout: # First login login_response = auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) assert login_response.status_code == 200 @@ -84,13 +86,13 @@ class TestAuthMe: # Login first auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) response = auth_client.get("/api/v1/auth/me") assert response.status_code == 200 data = response.json() - assert data["username"] == "admin" + assert data["username"] == get_admin_username() assert data["is_admin"] is True assert "id" in data assert "created_at" in data @@ -119,7 +121,7 @@ class TestAuthChangePassword: # Login as admin to create a test user auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) test_username = f"pwchange_{uuid4().hex[:8]}" auth_client.post( @@ -162,7 +164,7 @@ class TestAuthChangePassword: # Login as admin to create a test user auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) test_username = f"pwwrong_{uuid4().hex[:8]}" auth_client.post( @@ -194,7 +196,7 @@ class TestAPIKeys: # Login first auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) # Create API key @@ -226,7 +228,7 @@ class TestAPIKeys: # Login and create API key auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) create_response = auth_client.post( "/api/v1/auth/keys", @@ -242,12 +244,12 @@ class TestAPIKeys: headers={"Authorization": f"Bearer {api_key}"}, ) assert response.status_code == 200 - assert response.json()["username"] == "admin" + assert response.json()["username"] == get_admin_username() # Clean up auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) auth_client.delete(f"/api/v1/auth/keys/{key_id}") @@ -257,7 +259,7 @@ class TestAPIKeys: # Login and create API key auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) create_response = auth_client.post( "/api/v1/auth/keys", @@ -288,14 +290,14 @@ class TestAdminUserManagement: # Login as admin auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) response = auth_client.get("/api/v1/admin/users") assert response.status_code == 200 users = response.json() assert len(users) >= 1 - assert any(u["username"] == "admin" for u in users) + assert any(u["username"] == get_admin_username() for u in users) @pytest.mark.integration def test_create_user(self, auth_client): @@ -303,7 +305,7 @@ class TestAdminUserManagement: # Login as admin auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) # Create new user @@ -336,7 +338,7 @@ class TestAdminUserManagement: # Login as admin auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) # Create a test user @@ -362,7 +364,7 @@ class TestAdminUserManagement: # Login as admin auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) # Create a test user @@ -393,7 +395,7 @@ class TestAdminUserManagement: # Login as admin and create non-admin user auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) test_username = f"nonadmin_{uuid4().hex[:8]}" auth_client.post( @@ -423,7 +425,7 @@ class TestSecurityEdgeCases: # Login as admin and create a user auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) test_username = f"inactive_{uuid4().hex[:8]}" auth_client.post( @@ -451,7 +453,7 @@ class TestSecurityEdgeCases: """Test that short passwords are rejected when creating users.""" auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) response = auth_client.post( @@ -467,7 +469,7 @@ class TestSecurityEdgeCases: # Create test user auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) test_username = f"shortchange_{uuid4().hex[:8]}" auth_client.post( @@ -494,7 +496,7 @@ class TestSecurityEdgeCases: """Test that short passwords are rejected when resetting password.""" auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) # Create a test user first @@ -516,7 +518,7 @@ class TestSecurityEdgeCases: """Test that duplicate usernames are rejected.""" auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) test_username = f"duplicate_{uuid4().hex[:8]}" @@ -541,7 +543,7 @@ class TestSecurityEdgeCases: # Login as admin and create an API key auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) create_response = auth_client.post( "/api/v1/auth/keys", @@ -572,7 +574,7 @@ class TestSecurityEdgeCases: auth_client.cookies.clear() auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) auth_client.delete(f"/api/v1/auth/keys/{admin_key_id}") @@ -582,7 +584,7 @@ class TestSecurityEdgeCases: # Create a test user auth_client.post( "/api/v1/auth/login", - json={"username": "admin", "password": "changeme123"}, + json={"username": get_admin_username(), "password": get_admin_password()}, ) test_username = f"sessiontest_{uuid4().hex[:8]}" auth_client.post( diff --git a/backend/tests/unit/test_auth.py b/backend/tests/unit/test_auth.py new file mode 100644 index 0000000..cfe4157 --- /dev/null +++ b/backend/tests/unit/test_auth.py @@ -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 diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 6426494..9c92f7f 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -26,6 +26,8 @@ services: - ORCHARD_REDIS_PORT=6379 # Higher rate limit for local development/testing - 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: postgres: condition: service_healthy diff --git a/helm/orchard/templates/_helpers.tpl b/helm/orchard/templates/_helpers.tpl index d8d42a7..ea57cb6 100644 --- a/helm/orchard/templates/_helpers.tpl +++ b/helm/orchard/templates/_helpers.tpl @@ -141,3 +141,16 @@ MinIO secret name {{- printf "%s-s3-secret" (include "orchard.fullname" .) }} {{- 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 }} diff --git a/helm/orchard/templates/deployment.yaml b/helm/orchard/templates/deployment.yaml index 5bc6fa2..ef5376c 100644 --- a/helm/orchard/templates/deployment.yaml +++ b/helm/orchard/templates/deployment.yaml @@ -128,11 +128,27 @@ spec: value: {{ .Values.orchard.rateLimit.login | quote }} {{- 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: + {{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }} - name: db-secrets - mountPath: /mnt/secrets-store + mountPath: /mnt/secrets-store/db 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 }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} @@ -140,14 +156,24 @@ spec: {{- toYaml .Values.readinessProbe | nindent 12 }} resources: {{- 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: + {{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }} - name: db-secrets csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: 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 }} {{- with .Values.nodeSelector }} nodeSelector: diff --git a/helm/orchard/templates/secret-provider-class.yaml b/helm/orchard/templates/secret-provider-class.yaml index 1259b85..1a0bfed 100644 --- a/helm/orchard/templates/secret-provider-class.yaml +++ b/helm/orchard/templates/secret-provider-class.yaml @@ -25,3 +25,27 @@ spec: - objectName: db-password key: password {{- 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 }} diff --git a/helm/orchard/templates/secret.yaml b/helm/orchard/templates/secret.yaml index 994342a..32bbc14 100644 --- a/helm/orchard/templates/secret.yaml +++ b/helm/orchard/templates/secret.yaml @@ -22,3 +22,15 @@ data: access-key-id: {{ .Values.orchard.s3.accessKeyId | b64enc | quote }} secret-access-key: {{ .Values.orchard.s3.secretAccessKey | b64enc | quote }} {{- 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 }} diff --git a/helm/orchard/values-dev.yaml b/helm/orchard/values-dev.yaml index c15557d..8aafb1d 100644 --- a/helm/orchard/values-dev.yaml +++ b/helm/orchard/values-dev.yaml @@ -90,6 +90,10 @@ orchard: host: "0.0.0.0" port: 8080 + # Authentication settings + # Admin password is set via CI variable (DEV_ADMIN_PASSWORD) passed as --set flag + # This keeps the password out of version control + database: host: "" port: 5432 diff --git a/helm/orchard/values-prod.yaml b/helm/orchard/values-prod.yaml index ee77c2b..fb08ccb 100644 --- a/helm/orchard/values-prod.yaml +++ b/helm/orchard/values-prod.yaml @@ -93,6 +93,13 @@ orchard: host: "0.0.0.0" 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: host: "orchard-prd.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com" diff --git a/helm/orchard/values-stage.yaml b/helm/orchard/values-stage.yaml index 19e889e..7e8cbcf 100644 --- a/helm/orchard/values-stage.yaml +++ b/helm/orchard/values-stage.yaml @@ -95,6 +95,13 @@ orchard: host: "0.0.0.0" 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: host: "orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com" diff --git a/helm/orchard/values.yaml b/helm/orchard/values.yaml index cafea3a..393a422 100644 --- a/helm/orchard/values.yaml +++ b/helm/orchard/values.yaml @@ -120,6 +120,17 @@ orchard: mode: "presigned" # presigned, redirect, or proxy 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: enabled: true