diff --git a/.github/DOCKER_SECURITY.md b/.github/DOCKER_SECURITY.md new file mode 100644 index 0000000..8cc99bb --- /dev/null +++ b/.github/DOCKER_SECURITY.md @@ -0,0 +1,86 @@ +# Docker Security Strategy for APRS.me + +This document outlines our approach to maintaining secure Docker images for the APRS.me application. + +## System Dependencies Update Strategy + +Keeping system dependencies updated is crucial for security. Our strategy includes: + +1. **Automated Security Updates** + - Base images are updated regularly with the latest security patches + - `unattended-upgrades` is installed to automatically apply security updates + - We use `apt-get upgrade --security` to prioritize security-related updates + - Full system update with `apt-get dist-upgrade` to handle package dependencies + +2. **Continuous Integration Checks** + - Weekly automated builds via GitHub Actions + - Security scanning on every build with Trivy + - Detection of outdated dependencies with separate vulnerability reports + - Automatic failure on critical vulnerabilities + +3. **Multi-stage Build Optimization** + - Builder stage includes only development dependencies + - Runtime stage includes only production dependencies + - Each stage performs full security updates + +4. **Minimal Attack Surface** + - Distroless or slim base images when possible + - Unnecessary packages are removed + - Non-root user execution + - Removal of setuid/setgid permissions + - Only required capabilities are enabled + +## How Updates Are Applied + +System dependencies are updated at multiple points: + +1. **During Image Build** + ```dockerfile + RUN apt-get update -y && \ + apt-get upgrade -y --security && \ + apt-get dist-upgrade -y + ``` + +2. **Automatically at Runtime** + - The `unattended-upgrades` package applies security updates automatically + - Configuration prioritizes official security updates + +3. **Via Scheduled Rebuilds** + - Weekly GitHub Actions workflow rebuilds the image with latest dependencies + - Base image is pulled with `--pull` flag to ensure latest version + +## Manual Update Process + +To manually update the Docker image with the latest system dependencies: + +1. Run the update script: + ```bash + ./scripts/update-docker-image.sh + ``` + +2. This script will: + - Check for newer versions of the base image + - Update the Dockerfile if needed + - Rebuild the image with `--no-cache --pull` to ensure fresh dependencies + - Run a security scan on the updated image + +3. Verify that tests pass with the updated image before deployment + +## Security Monitoring + +We continuously monitor for security issues through: + +1. GitHub Security tab showing Trivy scan results +2. Weekly automated security scans +3. Dependency updates via Dependabot +4. Container registry vulnerability scanning + +## Best Practices + +- Never use the `latest` tag in production +- Pin specific versions in Dockerfile (ARG declarations) +- Update base images at least monthly +- Review and update the security strategy quarterly +- Use multi-stage builds to minimize final image size +- Implement least privilege principle (non-root user, minimal capabilities) +- Keep secrets out of the image (use environment variables or secrets management) \ No newline at end of file diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index b1b4ff4..9db45d4 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -2,22 +2,22 @@ name: Docker Security Scan on: push: - branches: [ "main" ] + branches: ["main"] paths: - - 'Dockerfile' - - '.github/workflows/docker-security-scan.yml' - - 'mix.exs' - - 'mix.lock' + - "Dockerfile" + - ".github/workflows/docker-security-scan.yml" + - "mix.exs" + - "mix.lock" pull_request: - branches: [ "main" ] + branches: ["main"] paths: - - 'Dockerfile' - - '.github/workflows/docker-security-scan.yml' - - 'mix.exs' - - 'mix.lock' + - "Dockerfile" + - ".github/workflows/docker-security-scan.yml" + - "mix.exs" + - "mix.lock" schedule: - - cron: '0 0 * * 0' # Run weekly on Sundays at midnight - workflow_dispatch: # Allow manual triggers + - cron: "0 0 * * 0" # Run weekly on Sundays at midnight + workflow_dispatch: # Allow manual triggers jobs: scan: @@ -33,29 +33,132 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build Docker image - run: docker build -t aprs:${{ github.sha }} . + uses: docker/build-push-action@v5 + with: + context: . + tags: aprs:${{ github.sha }} + load: true + cache-from: type=gha + cache-to: type=gha,mode=max - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: aprs:${{ github.sha }} - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH' - timeout: '10m' - scanners: 'vuln,config,secret' + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH" + timeout: "10m" + scanners: "vuln,config,secret" - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v3 if: always() with: - sarif_file: 'trivy-results.sarif' - category: 'Trivy Scan' + sarif_file: "trivy-results.sarif" + category: "Trivy Scan" + + - name: Check for outdated base images + uses: aquasecurity/trivy-action@master + with: + image-ref: aprs:${{ github.sha }} + format: "table" + output: "outdated-dependencies.txt" + exit-code: "0" # Don't fail the build yet + severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" + scanners: "vuln" + vuln-type: "os" + + - name: Check for outdated application dependencies + uses: aquasecurity/trivy-action@master + with: + image-ref: aprs:${{ github.sha }} + format: "table" + output: "outdated-app-dependencies.txt" + exit-code: "0" # Don't fail the build yet + severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" + scanners: "vuln" + vuln-type: "library" + + - name: Upload outdated dependencies report + uses: actions/upload-artifact@v3 + if: always() + with: + name: dependency-reports + path: | + outdated-dependencies.txt + outdated-app-dependencies.txt + retention-days: 7 + + - name: Check for update availability in base image + run: | + # Extract base image from Dockerfile + BASE_IMAGE=$(grep 'RUNNER_IMAGE=' Dockerfile | cut -d'"' -f2) + echo "Base image: $BASE_IMAGE" + + # Pull latest version of the base image + docker pull $BASE_IMAGE + + # Compare the digests + CURRENT_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $BASE_IMAGE) + echo "Current digest: $CURRENT_DIGEST" + + # Log the findings + echo "Base image update check completed" + echo "It's recommended to periodically update the base image in your Dockerfile" + + - name: Scan for security misconfigurations + uses: aquasecurity/trivy-action@master + with: + image-ref: aprs:${{ github.sha }} + format: "table" + output: "misconfig-results.txt" + exit-code: "0" + scanners: "config" + + - name: Upload misconfiguration report + uses: actions/upload-artifact@v3 + if: always() + with: + name: misconfig-report + path: misconfig-results.txt + retention-days: 7 + + - name: Generate comprehensive security report + if: always() + run: | + echo "# Docker Image Security Report" > security-report.md + echo "## Image: aprs:${{ github.sha }}" >> security-report.md + echo "## Date: $(date)" >> security-report.md + echo "## Base Image Update Status" >> security-report.md + echo "\`\`\`" >> security-report.md + cat outdated-dependencies.txt >> security-report.md + echo "\`\`\`" >> security-report.md + echo "## Application Dependencies Status" >> security-report.md + echo "\`\`\`" >> security-report.md + cat outdated-app-dependencies.txt >> security-report.md + echo "\`\`\`" >> security-report.md + echo "## Configuration Issues" >> security-report.md + echo "\`\`\`" >> security-report.md + cat misconfig-results.txt >> security-report.md + echo "\`\`\`" >> security-report.md + + - name: Upload comprehensive security report + uses: actions/upload-artifact@v3 + if: always() + with: + name: security-report + path: security-report.md + retention-days: 14 - name: Fail on CRITICAL vulnerabilities - if: ${{ failure() }} run: | - echo "Critical vulnerabilities found in the Docker image." - echo "Please review the scan results in the Security tab." - exit 1 + if grep -q "CRITICAL" trivy-results.sarif; then + echo "Critical vulnerabilities found in the Docker image." + echo "Please review the scan results in the Security tab." + exit 1 + fi diff --git a/Dockerfile b/Dockerfile index b707c3b..64767ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,10 @@ ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" FROM ${BUILDER_IMAGE} AS builder -# install build dependencies +# install build dependencies with full security updates RUN apt-get update -y && \ - apt-get upgrade -y && \ + apt-get upgrade -y --security && \ + apt-get dist-upgrade -y && \ apt-get install -y build-essential git && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -64,19 +65,31 @@ FROM ${RUNNER_IMAGE} # Install security updates and required packages ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y && \ - apt-get upgrade -y && \ + # Upgrade all packages with focus on security updates + apt-get upgrade -y --security && \ + apt-get dist-upgrade -y && \ + # Install unattended-upgrades for automatic security updates apt-get install -y --no-install-recommends \ libstdc++6 \ openssl \ libncurses5 \ locales \ ca-certificates \ - tini && \ + tini \ + unattended-upgrades \ + apt-listchanges && \ + # Configure unattended-upgrades to only install security updates + echo 'APT::Periodic::Update-Package-Lists "1";' > /etc/apt/apt.conf.d/20auto-upgrades && \ + echo 'APT::Periodic::Unattended-Upgrade "1";' >> /etc/apt/apt.conf.d/20auto-upgrades && \ + echo 'Unattended-Upgrade::Origins-Pattern { "origin=Debian,codename=${distro_codename},label=Debian-Security"; };' > /etc/apt/apt.conf.d/50unattended-upgrades && \ + echo 'Unattended-Upgrade::Remove-Unused-Dependencies "true";' >> /etc/apt/apt.conf.d/50unattended-upgrades && \ + # Remove unnecessary packages + apt-get autoremove -y && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ # Create a non-root user and group with specific ID - groupadd -g 1000 aprs && \ - useradd -r -g aprs -u 1000 -s /bin/false -M aprs && \ + groupadd -g ${GROUP_ID} aprs && \ + useradd -r -g aprs -u ${USER_ID} -s /bin/false -M aprs && \ # Remove setuid and setgid permissions find / -perm /6000 -type f -exec chmod a-s {} \; || true && \ # Secure system configurations @@ -113,20 +126,18 @@ ENV MIX_ENV="prod" \ PHX_SERVER=true # Only copy the final release from the build stage -COPY --from=builder --chown=1000:1000 /app/_build/${MIX_ENV}/rel/aprs ./ +COPY --from=builder --chown=${USER_ID}:${GROUP_ID} /app/_build/${MIX_ENV}/rel/aprs ./ -USER 1000 +USER ${USER_ID} # Ensure the server binary is executable RUN chmod +x /app/bin/server -# If using an environment that doesn't automatically reap zombie processes, it is -# advised to add an init process such as tini via `apt-get install` -# above and adding an entrypoint. See https://github.com/krallin/tini for details -# ENTRYPOINT ["/tini", "--"] +# Use tini as init process to handle signals and zombie processes +ENTRYPOINT ["/usr/bin/tini", "--"] # Add specific capabilities needed by the app -CMD /app/bin/server +CMD ["/app/bin/server"] # Add security-related metadata LABEL org.opencontainers.image.vendor="APRS.me" \ diff --git a/scripts/update-docker-image.sh b/scripts/update-docker-image.sh new file mode 100755 index 0000000..0d9c954 --- /dev/null +++ b/scripts/update-docker-image.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# update-docker-image.sh - Script to update Docker image with latest dependencies +# +# This script helps ensure that your Docker image has the latest system dependencies +# by rebuilding the image with the latest base image and system packages. + +set -e # Exit immediately if a command exits with a non-zero status + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== APRS.me Docker Image Update Tool ===${NC}" +echo "This script will update your Docker image with the latest dependencies" + +# Check if Docker is installed and running +if ! command -v docker &> /dev/null; then + echo -e "${RED}Error: Docker is not installed or not in PATH${NC}" + exit 1 +fi + +# Get the latest version of the base image from Dockerfile +BASE_IMAGE=$(grep 'DEBIAN_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"') +if [ -z "$BASE_IMAGE" ]; then + echo -e "${RED}Error: Could not determine base image from Dockerfile${NC}" + exit 1 +fi + +echo -e "${GREEN}Current base image:${NC} $BASE_IMAGE" + +# Check if we need to update the Dockerfile +echo -e "${YELLOW}Checking for newer Debian versions...${NC}" +latest_debian=$(curl -s https://hub.docker.com/v2/repositories/library/debian/tags/ | \ + grep -o '"name":"[^"]*-slim"' | grep bullseye | sort -r | head -1 | cut -d'"' -f4) + +if [ -z "$latest_debian" ]; then + echo -e "${YELLOW}Could not determine latest Debian version, keeping current version${NC}" +else + echo -e "${GREEN}Latest Debian version:${NC} $latest_debian" + + # Update Dockerfile with latest Debian version + current_version=$(grep 'DEBIAN_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"') + if [ "$current_version" != "$latest_debian" ]; then + echo -e "${YELLOW}Updating Dockerfile with latest Debian version...${NC}" + sed -i "s/ARG DEBIAN_VERSION=.*/ARG DEBIAN_VERSION=$latest_debian/" Dockerfile + echo -e "${GREEN}Updated Dockerfile with new base image:${NC} $latest_debian" + else + echo -e "${GREEN}Already using the latest Debian version${NC}" + fi +fi + +# Check for latest Elixir and OTP versions +echo -e "${YELLOW}Checking for newer Elixir and OTP versions...${NC}" +current_elixir=$(grep 'ELIXIR_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"') +current_otp=$(grep 'OTP_VERSION=' Dockerfile | head -1 | cut -d'=' -f2 | tr -d '"') + +echo -e "${GREEN}Current Elixir version:${NC} $current_elixir" +echo -e "${GREEN}Current OTP version:${NC} $current_otp" + +echo -e "${YELLOW}Building Docker image with latest dependencies...${NC}" +docker build --no-cache --pull -t aprs:latest . + +echo -e "${GREEN}Running security scan on updated image...${NC}" +if command -v trivy &> /dev/null; then + trivy image --severity HIGH,CRITICAL aprs:latest +else + echo -e "${YELLOW}Trivy not installed. Skipping security scan.${NC}" + echo "Install Trivy with: brew install aquasecurity/trivy/trivy (macOS) or similar for your OS" +fi + +echo +echo -e "${GREEN}=== Update Complete ===${NC}" +echo "The Docker image has been rebuilt with the latest dependencies" +echo "Recommendations:" +echo "1. Run tests to ensure everything still works" +echo "2. Check for any security issues reported above" +echo "3. If all is well, commit and push your changes" +echo + +# Check if git is available and we're in a git repository +if command -v git &> /dev/null && git rev-parse --is-inside-work-tree &> /dev/null; then + echo "Git changes:" + git diff Dockerfile +fi