feat(security): apply IP whitelisting to all endpoints including heal… #64
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Production Docker Build | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - 'docker/**' | |
| - '.github/workflows/prod.yml' | |
| workflow_dispatch: | |
| permissions: | |
| id-token: write # Required for OIDC authentication to AWS | |
| contents: read | |
| jobs: | |
| build-and-push: | |
| name: Build and Push Production Docker Image | |
| runs-on: ubuntu-latest | |
| outputs: | |
| image_uri_sha: ${{ steps.image.outputs.IMAGE_URI_SHA }} | |
| image_uri_latest: ${{ steps.image.outputs.IMAGE_URI_LATEST }} | |
| git_sha: ${{ steps.git.outputs.GIT_SHA }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Extract git SHA | |
| id: git | |
| run: | | |
| GIT_SHA=$(git rev-parse HEAD) | |
| echo "GIT_SHA=$GIT_SHA" >> $GITHUB_OUTPUT | |
| echo "Git SHA: $GIT_SHA" | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.14' | |
| - name: Install uv | |
| run: | | |
| curl -LsSf https://astral.sh/uv/install.sh | sh | |
| echo "$HOME/.cargo/bin" >> $GITHUB_PATH | |
| - name: Install Python dependencies | |
| working-directory: docker | |
| run: uv sync --all-extras | |
| - name: Configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v5 | |
| with: | |
| role-to-assume: arn:aws:iam::730278974607:role/github/GitHub-benchling-webhook | |
| aws-region: us-east-1 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| platforms: linux/amd64 | |
| - name: Build and push Docker image | |
| working-directory: docker | |
| run: make push-ci VERSION=${{ steps.git.outputs.GIT_SHA }} | |
| env: | |
| DOCKER_DEFAULT_PLATFORM: linux/amd64 | |
| AWS_REGION: us-east-1 | |
| - name: Get Docker image URIs | |
| id: image | |
| run: | | |
| AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) | |
| GIT_SHA="${{ steps.git.outputs.GIT_SHA }}" | |
| IMAGE_URI_SHA="${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/quiltdata/benchling:${GIT_SHA}" | |
| IMAGE_URI_LATEST="${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/quiltdata/benchling:latest" | |
| echo "IMAGE_URI_SHA=$IMAGE_URI_SHA" >> $GITHUB_OUTPUT | |
| echo "IMAGE_URI_LATEST=$IMAGE_URI_LATEST" >> $GITHUB_OUTPUT | |
| echo "Docker Images:" | |
| echo " SHA: $IMAGE_URI_SHA" | |
| echo " Latest: $IMAGE_URI_LATEST" | |
| validate: | |
| name: Validate Production Image | |
| runs-on: ubuntu-latest | |
| needs: build-and-push | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.14' | |
| - name: Install uv | |
| run: | | |
| curl -LsSf https://astral.sh/uv/install.sh | sh | |
| echo "$HOME/.cargo/bin" >> $GITHUB_PATH | |
| - name: Install Python dependencies | |
| working-directory: docker | |
| run: uv sync --all-extras | |
| - name: Configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v5 | |
| with: | |
| role-to-assume: arn:aws:iam::730278974607:role/github/GitHub-benchling-webhook | |
| aws-region: us-east-1 | |
| - name: Validate image architecture | |
| working-directory: docker | |
| run: | | |
| echo "Validating production image architecture..." | |
| DOCKER_IMAGE_NAME=quiltdata/benchling \ | |
| uv run python scripts/docker.py validate \ | |
| --version ${{ needs.build-and-push.outputs.git_sha }} \ | |
| --region us-east-1 | |
| env: | |
| AWS_REGION: us-east-1 | |
| - name: Validate application startup | |
| working-directory: docker | |
| run: | | |
| echo "Validating application starts successfully in degraded mode..." | |
| echo "(This mirrors 'npm run test:minimal' for local testing)" | |
| echo "" | |
| AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) | |
| IMAGE_URI="${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/quiltdata/benchling:${{ needs.build-and-push.outputs.git_sha }}" | |
| # Pull the image | |
| aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com | |
| docker pull ${IMAGE_URI} | |
| # Run container with minimal config to test startup (no valid secrets) | |
| # The application should start in degraded mode with health checks passing | |
| CONTAINER_ID=$(docker run -d \ | |
| -p 8080:8080 \ | |
| -e PORT=8080 \ | |
| -e AWS_REGION=us-east-1 \ | |
| -e PACKAGER_SQS_URL=https://sqs.us-east-1.amazonaws.com/000000000000/test-queue \ | |
| -e QUILT_WEB_HOST=https://example.quiltdata.com \ | |
| -e ATHENA_USER_DATABASE=test_db \ | |
| -e BenchlingSecret=test-secret \ | |
| ${IMAGE_URI}) | |
| # Wait up to 30 seconds for container to start | |
| echo "Waiting for Gunicorn to start..." | |
| for i in $(seq 1 30); do | |
| sleep 1 | |
| if docker logs ${CONTAINER_ID} 2>&1 | grep -q "Booting worker with pid"; then | |
| echo "✅ Gunicorn started successfully" | |
| echo "" | |
| echo "Container logs:" | |
| docker logs ${CONTAINER_ID} | |
| echo "" | |
| # Verify health endpoints work in degraded mode | |
| echo "Testing health endpoints..." | |
| if curl -f -s http://localhost:8080/health >/dev/null; then | |
| echo "✅ Health endpoint responds" | |
| HEALTH_STATUS=$(curl -s http://localhost:8080/health | jq -r '.status') | |
| if [ "$HEALTH_STATUS" = "healthy" ]; then | |
| echo "✅ Health check passed (degraded mode expected)" | |
| else | |
| echo "⚠️ Unexpected health status: $HEALTH_STATUS" | |
| fi | |
| else | |
| echo "❌ Health endpoint failed" | |
| docker stop ${CONTAINER_ID} >/dev/null 2>&1 || true | |
| docker rm ${CONTAINER_ID} >/dev/null 2>&1 || true | |
| exit 1 | |
| fi | |
| docker stop ${CONTAINER_ID} >/dev/null 2>&1 || true | |
| docker rm ${CONTAINER_ID} >/dev/null 2>&1 || true | |
| exit 0 | |
| fi | |
| done | |
| echo "❌ Gunicorn did not start within 30 seconds" | |
| echo "" | |
| echo "Container logs:" | |
| docker logs ${CONTAINER_ID} | |
| docker stop ${CONTAINER_ID} >/dev/null 2>&1 || true | |
| docker rm ${CONTAINER_ID} >/dev/null 2>&1 || true | |
| exit 1 | |
| env: | |
| AWS_REGION: us-east-1 | |
| - name: Validation summary | |
| run: | | |
| echo "## Validation Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ Image architecture validated: linux/amd64" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ Application starts successfully" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ Latest tag verified" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**SHA Image:**" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "${{ needs.build-and-push.outputs.image_uri_sha }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Latest Image:**" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "${{ needs.build-and-push.outputs.image_uri_latest }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| summary: | |
| name: Deployment Summary | |
| runs-on: ubuntu-latest | |
| needs: [build-and-push, validate] | |
| if: always() | |
| steps: | |
| - name: Build and deployment summary | |
| run: | | |
| echo "## Production Docker Build Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Git SHA:** \`${{ needs.build-and-push.outputs.git_sha }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "**Build Status:** ${{ needs.build-and-push.result }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Validation Status:** ${{ needs.validate.result }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ needs.build-and-push.result }}" == "success" ]; then | |
| echo "### Docker Images" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**SHA-tagged image:**" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "${{ needs.build-and-push.outputs.image_uri_sha }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Latest-tagged image:**" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "${{ needs.build-and-push.outputs.image_uri_latest }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Usage" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Reference in deployments:" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`yaml" >> $GITHUB_STEP_SUMMARY | |
| echo "image: ${{ needs.build-and-push.outputs.image_uri_sha }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ Build failed. Check job logs for details." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.validate.result }}" != "success" ]; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "⚠️ Validation failed. Image may not meet production requirements." >> $GITHUB_STEP_SUMMARY | |
| fi |