diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b05d3da --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,19 @@ +# CODEOWNERS file +# These owners will be the default owners for everything in the repo + +# Global owners +* @efigueroa + +# Core infrastructure requires careful review +/compose/core/ @efigueroa + +# Security-sensitive files +**/.env @efigueroa +.github/workflows/security-checks.yml @efigueroa + +# Documentation +/README.md @efigueroa +/docs/ @efigueroa + +# CI/CD +/.github/ @efigueroa diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..7f8d9d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,64 @@ +--- +name: Bug Report +about: Report a bug or issue with a service +title: '[BUG] ' +labels: 'bug' +assignees: '' +--- + +## Bug Description + + + +## Service Affected + +**Service Name:** + +**Service Location:** (e.g., compose/media/frontend/jellyfin) + +## Steps to Reproduce + +1. +2. +3. + +## Expected Behavior + + + +## Actual Behavior + + + +## Logs + +``` +# Paste relevant logs here +# docker compose logs [service] +``` + +## Environment + +- Docker version: +- Docker Compose version: +- Host OS: + +## Configuration + +**compose.yaml:** +```yaml +# Paste relevant parts of your compose file +``` + +**.env (sanitized):** +```bash +# Paste relevant environment variables (remove passwords!) +``` + +## Additional Context + + + +## Possible Solution + + diff --git a/.github/ISSUE_TEMPLATE/service-request.md b/.github/ISSUE_TEMPLATE/service-request.md new file mode 100644 index 0000000..2f9bc18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/service-request.md @@ -0,0 +1,63 @@ +--- +name: Service Request +about: Request a new service to be added to the homelab +title: '[SERVICE] ' +labels: 'enhancement, service-request' +assignees: '' +--- + +## Service Information + +**Service Name:** + +**Official Website/Docs:** + +**Docker Image:** + +**Category:** (core/media/services) + +## Description + + + +## Configuration Requirements + +**Required Environment Variables:** +``` +VAR_NAME=description +``` + +**Required Volumes:** +``` +/path/to/data:/container/path +``` + +**Required Ports:** +- Port: (if any need to be exposed) + +**Dependencies:** +- Database: (postgres/mysql/redis/etc.) +- Other services: + +## Integration Requirements + +**Domain Preference:** + + +**SSO Protected:** Yes/No + +**Access to /media folders:** (list which ones if needed) +- [ ] /media/movies +- [ ] /media/tv +- [ ] /media/music +- [ ] /media/books +- [ ] /media/photos +- [ ] Other: + +## Additional Context + + + +## Security Considerations + + diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..88a8c82 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,47 @@ +# Auto labeler configuration + +'category: core': + - compose/core/**/* + +'category: media': + - compose/media/**/* + +'category: services': + - compose/services/**/* + +'type: documentation': + - '**/*.md' + - docs/**/* + +'type: configuration': + - '**/*.yaml' + - '**/*.yml' + - '**/*.env' + +'type: ci/cd': + - .github/**/* + - .pre-commit-config.yaml + +'security': + - '**/*.env' + - '**/secrets/**/*' + +'traefik': + - compose/core/traefik/**/* + - any: + - changed-files: + - any-glob-to-any-file: 'compose/**/compose.yaml' + changed-lines: + - pattern: 'traefik\.' + +'authentication': + - compose/core/lldap/**/* + - compose/core/tinyauth/**/* + +'dependencies': + - '**/compose.yaml' + - any: + - changed-files: + - any-glob-to-any-file: 'compose/**/compose.yaml' + changed-lines: + - pattern: 'image:' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..141effb --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,96 @@ +## Description + + + +## Type of Change + + + +- [ ] New service addition +- [ ] Service configuration update +- [ ] Bug fix +- [ ] Documentation update +- [ ] Security fix +- [ ] Infrastructure change + +## Changes Made + + + +- +- +- + +## Checklist + + + +### General +- [ ] All compose files use `compose.yaml` (not `.yml`) +- [ ] Code follows Docker Compose best practices +- [ ] Changes tested locally +- [ ] Documentation updated (README.md) + +### Services (if applicable) +- [ ] Service added to correct category (core/media/services) +- [ ] Proper network configuration (homelab + internal if needed) +- [ ] Volumes properly configured +- [ ] Environment variables use `.env` file or are documented + +### Traefik & SSL (if applicable) +- [ ] Traefik labels configured correctly +- [ ] Uses `websecure` entrypoint +- [ ] Let's Encrypt cert resolver configured +- [ ] Both domains configured (`fig.systems` and `edfig.dev`) +- [ ] SSO middleware applied (if appropriate) + +### Security +- [ ] No secrets committed in `.env` files +- [ ] Placeholder passwords use `changeme_*` format +- [ ] No sensitive data in compose files +- [ ] Container runs as non-root user (where possible) + +### Documentation +- [ ] Service added to README.md service table +- [ ] Deployment instructions added/updated +- [ ] Configuration requirements documented +- [ ] Comments added to compose file explaining purpose + +## Testing + + + +```bash +# Commands used to test: + + +# Expected behavior: + + +# Actual behavior: + +``` + +## Screenshots (if applicable) + + + +## Related Issues + + + +## Additional Notes + + + +--- + +## For Reviewers + + + +- [ ] All CI checks pass +- [ ] Docker Compose validation passes +- [ ] YAML linting passes +- [ ] Security scans pass +- [ ] No security vulnerabilities introduced diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000..7901ba0 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,44 @@ +name: Auto Label + +on: + pull_request: + types: [opened, edited, synchronize] + +jobs: + label-pr: + name: Auto Label PR + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Label based on changed files + uses: actions/labeler@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: .github/labeler.yml + + size-label: + name: PR Size Label + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Label by size + uses: codelytv/pr-size-labeler@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + xs_label: 'size/xs' + xs_max_size: 10 + s_label: 'size/s' + s_max_size: 100 + m_label: 'size/m' + m_max_size: 500 + l_label: 'size/l' + l_max_size: 1000 + xl_label: 'size/xl' diff --git a/.github/workflows/docker-compose-validation.yml b/.github/workflows/docker-compose-validation.yml new file mode 100644 index 0000000..98f22d0 --- /dev/null +++ b/.github/workflows/docker-compose-validation.yml @@ -0,0 +1,159 @@ +name: Docker Compose Validation + +on: + pull_request: + paths: + - 'compose/**/*.yaml' + - 'compose/**/*.yml' + - '.github/workflows/docker-compose-validation.yml' + push: + branches: + - main + paths: + - 'compose/**/*.yaml' + - 'compose/**/*.yml' + +jobs: + validate-compose-files: + name: Validate Docker Compose Files + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Find all compose files + id: find-files + run: | + echo "Finding all compose.yaml files..." + find compose -name "compose.yaml" -type f > compose-files.txt + cat compose-files.txt + echo "Found $(wc -l < compose-files.txt) compose files" + + - name: Validate compose file syntax + run: | + echo "Validating Docker Compose files..." + exit_code=0 + + while IFS= read -r file; do + echo "Validating: $file" + if docker compose -f "$file" config > /dev/null 2>&1; then + echo "✅ Valid: $file" + else + echo "❌ Invalid: $file" + docker compose -f "$file" config + exit_code=1 + fi + done < compose-files.txt + + exit $exit_code + + - name: Check for old .yml files + run: | + old_files=$(find compose -name "compose.yml" -o -name "docker-compose.yml" 2>/dev/null || true) + if [ -n "$old_files" ]; then + echo "❌ Found deprecated .yml files (should be .yaml):" + echo "$old_files" + exit 1 + else + echo "✅ All compose files use .yaml extension" + fi + + - name: Validate network references + run: | + echo "Checking network references..." + exit_code=0 + + while IFS= read -r file; do + # Check if file uses 'homelab' network + if grep -q "networks:" "$file"; then + if grep -q "homelab" "$file"; then + # Ensure it's marked as external + if grep -A 5 "^networks:" "$file" | grep -A 2 "homelab:" | grep -q "external: true"; then + echo "✅ $file: homelab network properly marked as external" + else + echo "⚠️ $file: homelab network should be marked as external: true" + exit_code=1 + fi + fi + fi + done < compose-files.txt + + exit $exit_code + + - name: Check for exposed ports + run: | + echo "Checking for unnecessary port exposures..." + files_with_ports=$(grep -l "ports:" compose/**/compose.yaml || true) + + if [ -n "$files_with_ports" ]; then + echo "⚠️ Files with exposed ports (consider using Traefik only):" + echo "$files_with_ports" + echo "" + echo "This is a warning, not an error. Review if these ports need to be exposed." + else + echo "✅ No ports exposed (using Traefik for all routing)" + fi + + - name: Validate Traefik labels + run: | + echo "Validating Traefik labels..." + exit_code=0 + + while IFS= read -r file; do + if grep -q "traefik.enable: true" "$file"; then + # Check for required labels + has_router=$(grep -q "traefik.http.routers\." "$file" && echo "yes" || echo "no") + has_entrypoint=$(grep -q "entrypoints: websecure" "$file" && echo "yes" || echo "no") + has_tls=$(grep -q "tls.certresolver: letsencrypt" "$file" && echo "yes" || echo "no") + has_rule=$(grep -q "\.rule: Host" "$file" && echo "yes" || echo "no") + + if [ "$has_router" = "yes" ] && [ "$has_entrypoint" = "yes" ] && [ "$has_tls" = "yes" ] && [ "$has_rule" = "yes" ]; then + echo "✅ $file: Complete Traefik configuration" + else + echo "⚠️ $file: Incomplete Traefik configuration" + [ "$has_router" = "no" ] && echo " - Missing router definition" + [ "$has_entrypoint" = "no" ] && echo " - Missing websecure entrypoint" + [ "$has_tls" = "no" ] && echo " - Missing TLS/Let's Encrypt config" + [ "$has_rule" = "no" ] && echo " - Missing Host rule" + exit_code=1 + fi + fi + done < compose-files.txt + + exit $exit_code + + - name: Check domain consistency + run: | + echo "Checking domain consistency..." + + # Extract all domains from Traefik rules + domains=$(grep -h "rule: Host" compose/**/compose.yaml | grep -oP '`\K[^`]+' | sort -u) + + echo "Configured domains:" + echo "$domains" + + # Check that both fig.systems and edfig.dev are used + fig_systems_count=$(echo "$domains" | grep -c "fig.systems" || true) + edfig_dev_count=$(echo "$domains" | grep -c "edfig.dev" || true) + + echo "" + echo "fig.systems domains: $fig_systems_count" + echo "edfig.dev domains: $edfig_dev_count" + + # Check for services that don't have both domains + while IFS= read -r file; do + if grep -q "traefik.enable: true" "$file"; then + has_fig_systems=$(grep "rule: Host" "$file" | grep -c "fig.systems" || true) + has_edfig_dev=$(grep "rule: Host" "$file" | grep -c "edfig.dev" || true) + + if [ "$has_fig_systems" -gt 0 ] && [ "$has_edfig_dev" -eq 0 ]; then + echo "⚠️ $file: Has fig.systems but missing edfig.dev" + elif [ "$has_edfig_dev" -gt 0 ] && [ "$has_fig_systems" -eq 0 ]; then + echo "⚠️ $file: Has edfig.dev but missing fig.systems" + fi + fi + done < compose-files.txt diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..027841f --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,177 @@ +name: Documentation Checks + +on: + pull_request: + paths: + - '**.md' + - 'compose/**/*.yaml' + - '.github/workflows/documentation.yml' + push: + branches: + - main + +jobs: + markdown-lint: + name: Markdown Linting + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Lint Markdown files + uses: nosborn/github-action-markdown-cli@v3.3.0 + with: + files: . + config_file: .markdownlint.json + ignore_files: node_modules/ + continue-on-error: true # Don't fail CI on markdown issues + + link-check: + name: Check Links in Documentation + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check links in README + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-quiet-mode: 'yes' + config-file: '.markdown-link-check.json' + continue-on-error: true + + readme-sync: + name: Validate README is up-to-date + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check README completeness + run: | + echo "Checking if README.md documents all services..." + + # Extract service names from compose files + services=$(find compose -name "compose.yaml" -exec dirname {} \; | \ + sed 's|compose/||' | \ + sort -u) + + echo "Services found in repository:" + echo "$services" + + missing_services="" + + # Check if each service is mentioned in README + while IFS= read -r service; do + service_name=$(basename "$service") + + if ! grep -qi "$service_name" README.md; then + echo "⚠️ Service '$service_name' not found in README.md" + missing_services="${missing_services}${service}\n" + fi + done <<< "$services" + + if [ -n "$missing_services" ]; then + echo "" + echo "⚠️ The following services are not documented in README.md:" + echo -e "$missing_services" + echo "" + echo "This is a warning. Consider updating the README." + else + echo "✅ All services are documented in README.md" + fi + + - name: Check for required sections + run: | + echo "Checking for required README sections..." + + required_sections=( + "Infrastructure" + "Directory Structure" + "Domains" + "Deployment" + "Security" + ) + + missing_sections="" + + for section in "${required_sections[@]}"; do + if ! grep -qi "$section" README.md; then + echo "❌ Missing section: $section" + missing_sections="${missing_sections}${section}\n" + else + echo "✅ Found section: $section" + fi + done + + if [ -n "$missing_sections" ]; then + echo "" + echo "❌ Missing required sections in README.md:" + echo -e "$missing_sections" + exit 1 + fi + + service-documentation: + name: Check Service Documentation + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check for service-level documentation + run: | + echo "Checking that compose files have documentation comments..." + + compose_files=$(find compose -name "compose.yaml" -type f) + + for file in $compose_files; do + # Check if file has a comment at the top + if head -n 1 "$file" | grep -q "^#"; then + echo "✅ $file has documentation" + else + echo "⚠️ $file missing documentation header" + fi + + # Check for docs URLs + if grep -q "# Docs:" "$file"; then + echo " ✅ Has docs link" + fi + done + + - name: Validate domain URLs in README + run: | + echo "Validating service URLs in README match compose files..." + + # Extract URLs from README (simple check) + readme_domains=$(grep -oP '\b\w+\.fig\.systems\b' README.md | sort -u) + + echo "Domains in README:" + echo "$readme_domains" + + # Extract domains from compose files + compose_domains=$(grep -h "rule: Host" compose/**/compose.yaml | \ + grep -oP '\b\w+\.fig\.systems\b' | \ + sort -u) + + echo "" + echo "Domains in compose files:" + echo "$compose_domains" + + # Basic comparison + readme_count=$(echo "$readme_domains" | wc -l) + compose_count=$(echo "$compose_domains" | wc -l) + + echo "" + echo "README domains: $readme_count" + echo "Compose domains: $compose_count" + + if [ "$readme_count" -ne "$compose_count" ]; then + echo "⚠️ Domain count mismatch between README and compose files" + echo "This may indicate outdated documentation." + else + echo "✅ Domain counts match" + fi diff --git a/.github/workflows/security-checks.yml b/.github/workflows/security-checks.yml new file mode 100644 index 0000000..dc2243f --- /dev/null +++ b/.github/workflows/security-checks.yml @@ -0,0 +1,193 @@ +name: Security Checks + +on: + pull_request: + push: + branches: + - main + +jobs: + secret-scanning: + name: Secret Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run Gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_ENABLE_COMMENTS: false + + env-file-validation: + name: Environment File Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check for placeholder passwords + run: | + echo "Checking for placeholder passwords in .env files..." + exit_code=0 + + # Find all .env files + env_files=$(find compose -name ".env" -type f) + + for env_file in $env_files; do + echo "Checking: $env_file" + + # Check for common placeholder passwords + if grep -E "(changeme|password123|admin123|test123|example)" "$env_file" > /dev/null 2>&1; then + echo "⚠️ WARNING: $env_file contains placeholder passwords" + echo " This is expected in the repository template." + echo " Users should change these before deployment." + fi + + # Check for actual leaked passwords (common patterns) + if grep -E "PASSWORD=.{20,}" "$env_file" | grep -v "changeme" > /dev/null 2>&1; then + echo "❌ ERROR: $env_file may contain a real password!" + exit_code=1 + fi + done + + exit $exit_code + + - name: Validate environment file format + run: | + echo "Validating .env file format..." + exit_code=0 + + env_files=$(find compose -name ".env" -type f) + + for env_file in $env_files; do + # Check for common .env issues + if grep -E "^\s+[A-Z_]+=.*" "$env_file" > /dev/null 2>&1; then + echo "❌ $env_file: Contains indented variables (should not be indented)" + exit_code=1 + fi + + if grep -E "^[A-Z_]+=\s+.*" "$env_file" > /dev/null 2>&1; then + echo "⚠️ $env_file: Contains space after = (may cause issues)" + fi + + # Check for commented-out critical variables + if grep -E "^#\s*(DATABASE_URL|POSTGRES_PASSWORD|JWT_SECRET|SESSION_SECRET)=" "$env_file" > /dev/null 2>&1; then + echo "⚠️ $env_file: Critical variables are commented out" + fi + done + + if [ $exit_code -eq 0 ]; then + echo "✅ All .env files are properly formatted" + fi + + exit $exit_code + + dockerfile-security: + name: Dockerfile Security Scan + runs-on: ubuntu-latest + if: false # Disabled since we use pre-built images, enable if you add custom Dockerfiles + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: "**/Dockerfile" + failure-threshold: warning + + container-image-scan: + name: Container Image Vulnerability Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract images from compose files + id: extract-images + run: | + echo "Extracting container images..." + + # Extract unique images from all compose files + images=$(grep -h "^\s*image:" compose/**/compose.yaml | \ + awk '{print $2}' | \ + grep -v "^$" | \ + sort -u) + + echo "Found images:" + echo "$images" + + # Save to file for next step + echo "$images" > images.txt + + # Count images + image_count=$(echo "$images" | wc -l) + echo "total_images=$image_count" >> $GITHUB_OUTPUT + + - name: Scan critical images with Trivy + run: | + # Install Trivy + sudo apt-get install wget apt-transport-https gnupg lsb-release + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install trivy + + # Scan a sample of critical images (to avoid long CI times) + critical_images=( + "traefik:v3.3" + "lldap/lldap:stable" + "ghcr.io/steveiliop56/tinyauth:latest" + "postgres:16-alpine" + "postgres:18" + "redis:alpine" + ) + + echo "Scanning critical images for HIGH and CRITICAL vulnerabilities..." + + for image in "${critical_images[@]}"; do + # Check if image is used in our compose files + if grep -q "$image" images.txt 2>/dev/null; then + echo "Scanning: $image" + trivy image --severity HIGH,CRITICAL --exit-code 0 "$image" || true + fi + done + + - name: Generate security report + if: always() + run: | + echo "# Security Scan Summary" > security-report.md + echo "" >> security-report.md + echo "## Images Scanned" >> security-report.md + echo "Total unique images: $(cat images.txt | wc -l)" >> security-report.md + echo "" >> security-report.md + echo "See job logs for detailed vulnerability information." >> security-report.md + + - name: Upload security report + if: always() + uses: actions/upload-artifact@v4 + with: + name: security-report + path: security-report.md + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml new file mode 100644 index 0000000..7e81f20 --- /dev/null +++ b/.github/workflows/yaml-lint.yml @@ -0,0 +1,135 @@ +name: YAML Linting + +on: + pull_request: + paths: + - '**.yaml' + - '**.yml' + - '.yamllint.yml' + push: + branches: + - main + paths: + - '**.yaml' + - '**.yml' + +jobs: + yamllint: + name: YAML Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install yamllint + run: pip install yamllint + + - name: Run yamllint + run: | + yamllint -f colored compose/ + + - name: Check YAML file extensions + run: | + echo "Checking for consistent YAML file extensions..." + + # Find all YAML files + yaml_files=$(find . -name "*.yaml" -o -name "*.yml" | grep -v ".git" | grep -v "node_modules") + + # Count by extension + yaml_count=$(find . -name "*.yaml" | grep -v ".git" | wc -l) + yml_count=$(find . -name "*.yml" | grep -v ".git" | wc -l) + + echo "Files with .yaml extension: $yaml_count" + echo "Files with .yml extension: $yml_count" + + # Check for any .yml files in compose directory (should be .yaml) + yml_in_compose=$(find compose -name "*.yml" 2>/dev/null | wc -l) + + if [ $yml_in_compose -gt 0 ]; then + echo "❌ Found .yml files in compose directory (should be .yaml):" + find compose -name "*.yml" + exit 1 + else + echo "✅ All compose files use .yaml extension" + fi + + validate-yaml-structure: + name: Validate YAML Structure + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Validate YAML syntax + run: | + echo "Validating YAML syntax for all files..." + exit_code=0 + + yaml_files=$(find compose -name "*.yaml" -type f) + + for file in $yaml_files; do + if yq eval '.' "$file" > /dev/null 2>&1; then + echo "✅ Valid YAML: $file" + else + echo "❌ Invalid YAML: $file" + yq eval '.' "$file" + exit_code=1 + fi + done + + exit $exit_code + + - name: Check for Docker Compose version + run: | + echo "Checking for version field in compose files..." + + yaml_files=$(find compose -name "compose.yaml" -type f) + + for file in $yaml_files; do + # Docker Compose v2+ doesn't require version field + # But check if it's present and warn if it's old + version=$(yq eval '.version // "none"' "$file") + + if [ "$version" != "none" ]; then + echo "⚠️ $file: Contains version field (not needed in Compose v2+)" + if [ "$version" = "2" ] || [ "$version" = "2.0" ]; then + echo " Consider removing or updating to version 3+" + fi + fi + done + + - name: Validate service names + run: | + echo "Checking service naming conventions..." + + yaml_files=$(find compose -name "compose.yaml" -type f) + + for file in $yaml_files; do + services=$(yq eval '.services | keys | .[]' "$file" 2>/dev/null) + + for service in $services; do + # Check for invalid characters in service names + if echo "$service" | grep -qE '[^a-zA-Z0-9_-]'; then + echo "❌ $file: Service '$service' has invalid characters" + echo " Service names should only contain: a-z, A-Z, 0-9, _, -" + exit 1 + fi + + # Check for recommended naming (lowercase with hyphens) + if echo "$service" | grep -qE '[A-Z]'; then + echo "⚠️ $file: Service '$service' contains uppercase (consider lowercase with hyphens)" + fi + done + done diff --git a/.markdown-link-check.json b/.markdown-link-check.json new file mode 100644 index 0000000..c52aed5 --- /dev/null +++ b/.markdown-link-check.json @@ -0,0 +1,18 @@ +{ + "ignorePatterns": [ + { + "pattern": "^http://localhost" + }, + { + "pattern": "^https://.*\\.fig\\.systems" + }, + { + "pattern": "^https://.*\\.edfig\\.dev" + } + ], + "timeout": "20s", + "retryOn429": true, + "retryCount": 3, + "fallbackRetryDelay": "30s", + "aliveStatusCodes": [200, 206, 429] +} diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..7f9f3e2 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,13 @@ +{ + "default": true, + "MD013": { + "line_length": 200, + "code_blocks": false, + "tables": false + }, + "MD033": false, + "MD041": false, + "MD024": { + "siblings_only": true + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..89e4baf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,65 @@ +# Pre-commit hooks for homelab repository +# Install: pip install pre-commit +# Setup: pre-commit install +# Run manually: pre-commit run --all-files + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + exclude: '.md$' + - id: end-of-file-fixer + - id: check-yaml + args: ['--allow-multiple-documents'] + - id: check-added-large-files + args: ['--maxkb=1000'] + - id: check-merge-conflict + - id: detect-private-key + - id: mixed-line-ending + + - repo: https://github.com/adrienverge/yamllint + rev: v1.35.1 + hooks: + - id: yamllint + args: ['-c', '.yamllint.yml'] + files: \.(yaml|yml)$ + + - repo: https://github.com/gitleaks/gitleaks + rev: v8.18.2 + hooks: + - id: gitleaks + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.39.0 + hooks: + - id: markdownlint + args: ['--config', '.markdownlint.json'] + + - repo: local + hooks: + - id: check-compose-filenames + name: Check compose file naming + entry: bash -c 'find compose -name "compose.yml" -o -name "docker-compose.yml" | grep . && exit 1 || exit 0' + language: system + pass_filenames: false + always_run: true + + - id: check-placeholder-passwords + name: Check for non-placeholder passwords + entry: bash -c 'git diff --cached --name-only | grep "\.env$" | xargs grep -E "PASSWORD=.{20,}" | grep -v changeme && exit 1 || exit 0' + language: system + pass_filenames: false + always_run: false + + - id: validate-traefik-labels + name: Validate Traefik labels + entry: bash -c 'for file in $(git diff --cached --name-only | grep "compose.yaml$"); do if grep -q "traefik.enable: true" "$file"; then grep -q "entrypoints: websecure" "$file" || { echo "Missing websecure entrypoint in $file"; exit 1; }; fi; done' + language: system + pass_filenames: false + + - id: check-env-files + name: Check .env files exist for services with env_file + entry: bash -c 'for file in $(git diff --cached --name-only | grep "compose.yaml$"); do if grep -q "env_file:" "$file"; then dir=$(dirname "$file"); if [ ! -f "$dir/.env" ]; then echo "Missing .env file for $file"; exit 1; fi; fi; done' + language: system + pass_filenames: false diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..82af7b2 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,53 @@ +--- +# yamllint configuration for Docker Compose files + +extends: default + +rules: + # Line length - Docker Compose files can have long lines (especially for commands) + line-length: + max: 200 + level: warning + + # Allow multiple spaces for alignment + colons: + max-spaces-after: 1 + + # Indentation - Docker Compose uses 2 spaces + indentation: + spaces: 2 + indent-sequences: true + + # Comments + comments: + min-spaces-from-content: 2 + + # Document start - not required for Docker Compose + document-start: disable + + # Allow truthy values for Docker Compose (yes/no, true/false, on/off) + truthy: + allowed-values: ['true', 'false', 'yes', 'no', 'on', 'off'] + check-keys: false + + # Brackets + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + + # Allow empty values + empty-values: + forbid-in-block-mappings: false + forbid-in-flow-mappings: false + + # Key ordering - not enforced + key-ordering: disable + + # Allow duplicate keys (sometimes needed in Docker labels) + key-duplicates: + forbid-duplicated-merge-keys: true + +ignore: | + .github/ + node_modules/ + venv/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a0adb3d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,264 @@ +# Contributing Guide + +Thank you for your interest in contributing to this homelab configuration! While this is primarily a personal repository, contributions are welcome. + +## How to Contribute + +### Reporting Issues + +- Use the [bug report template](.github/ISSUE_TEMPLATE/bug-report.md) for bugs +- Use the [service request template](.github/ISSUE_TEMPLATE/service-request.md) for new services +- Search existing issues before creating a new one +- Provide as much detail as possible + +### Submitting Changes + +1. **Fork the repository** +2. **Create a feature branch** + ```bash + git checkout -b feature/your-feature-name + ``` +3. **Make your changes** following the guidelines below +4. **Test your changes** locally +5. **Commit with clear messages** + ```bash + git commit -m "feat: add new service" + ``` +6. **Push to your fork** + ```bash + git push origin feature/your-feature-name + ``` +7. **Open a Pull Request** using the PR template + +## Guidelines + +### File Naming + +- All Docker Compose files must be named `compose.yaml` (not `.yml`) +- Use lowercase with hyphens for service directories (e.g., `calibre-web`) +- Environment files must be named `.env` + +### Docker Compose Best Practices + +- Use version-pinned images when possible +- Include health checks for databases and critical services +- Use bind mounts for configuration, named volumes for data +- Set proper restart policies (`unless-stopped` or `always`) +- Include resource limits for production services + +### Network Configuration + +- All services must use the `homelab` network (marked as `external: true`) +- Services with multiple containers should use an internal network +- Example: + ```yaml + networks: + homelab: + external: true + service_internal: + name: service_internal + driver: bridge + ``` + +### Traefik Labels + +All web services must include: + +```yaml +labels: + traefik.enable: true + traefik.http.routers.service.rule: Host(`service.fig.systems`) || Host(`service.edfig.dev`) + traefik.http.routers.service.entrypoints: websecure + traefik.http.routers.service.tls.certresolver: letsencrypt + traefik.http.services.service.loadbalancer.server.port: 8080 + # Optional SSO: + traefik.http.routers.service.middlewares: tinyauth +``` + +### Environment Variables + +- Use `.env` files for configuration +- Never commit real passwords +- Use `changeme_*` prefix for placeholder passwords +- Document all required environment variables +- Include comments explaining non-obvious settings + +### Documentation + +- Add service to README.md service table +- Include deployment instructions +- Document any special configuration +- Add comments to compose files explaining purpose +- Include links to official documentation + +### Security + +- Never commit secrets +- Scan compose files for vulnerabilities +- Use official or well-maintained images +- Enable SSO when appropriate +- Document security considerations + +## Code Style + +### YAML Style + +- 2-space indentation +- No trailing whitespace +- Use `true/false` instead of `yes/no` +- Quote strings with special characters +- Follow yamllint rules in `.yamllint.yml` + +### Commit Messages + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +- `feat:` New feature +- `fix:` Bug fix +- `docs:` Documentation changes +- `refactor:` Code refactoring +- `security:` Security improvements +- `chore:` Maintenance tasks + +Examples: +``` +feat: add jellyfin media server +fix: correct traefik routing for sonarr +docs: update README with new services +security: update postgres to latest version +``` + +## Testing + +Before submitting a PR: + +1. **Validate compose files** + ```bash + docker compose -f compose/path/to/compose.yaml config + ``` + +2. **Check YAML syntax** + ```bash + yamllint compose/ + ``` + +3. **Test locally** + ```bash + docker compose up -d + docker compose logs + ``` + +4. **Check for secrets** + ```bash + git diff --cached | grep -i "password\|secret\|token" + ``` + +5. **Run pre-commit hooks** (optional) + ```bash + pre-commit install + pre-commit run --all-files + ``` + +## Pull Request Process + +1. Fill out the PR template completely +2. Ensure all CI checks pass +3. Request review if needed +4. Address review feedback +5. Squash commits if requested +6. Wait for approval and merge + +## CI/CD Checks + +Your PR will be automatically checked for: + +- Docker Compose validation +- YAML linting +- Security scanning +- Secret detection +- Documentation completeness +- Traefik configuration +- Network setup +- File naming conventions + +Fix any failures before requesting review. + +## Adding a New Service + +1. Choose the correct category: + - `compose/core/` - Infrastructure (Traefik, auth, etc.) + - `compose/media/` - Media-related services + - `compose/services/` - Utility services + +2. Create service directory: + ```bash + mkdir -p compose/category/service-name + ``` + +3. Create `compose.yaml`: + - Include documentation header + - Add Traefik labels + - Configure networks + - Set up volumes + - Add health checks if applicable + +4. Create `.env` if needed: + - Use placeholder passwords + - Document all variables + - Include comments + +5. Update README.md: + - Add to service table + - Include URL + - Document deployment + +6. Test deployment: + ```bash + cd compose/category/service-name + docker compose up -d + docker compose logs -f + ``` + +7. Create PR with detailed description + +## Project Structure + +``` +homelab/ +├── .github/ +│ ├── workflows/ # CI/CD workflows +│ ├── ISSUE_TEMPLATE/ # Issue templates +│ └── pull_request_template.md +├── compose/ +│ ├── core/ # Infrastructure services +│ ├── media/ # Media services +│ └── services/ # Utility services +├── README.md # Main documentation +├── CONTRIBUTING.md # This file +├── SECURITY.md # Security policy +└── .yamllint.yml # YAML linting config +``` + +## Getting Help + +- Check existing issues and PRs +- Review the README.md +- Examine similar services for examples +- Ask in PR comments + +## License + +By contributing, you agree that your contributions will be licensed under the same terms as the repository. + +## Code of Conduct + +- Be respectful and professional +- Focus on constructive feedback +- Help others learn and improve +- Keep discussions relevant + +## Questions? + +Open an issue with the question label or comment on an existing PR/issue. + +Thank you for contributing! 🎉 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5fbcbf1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,144 @@ +# Security Policy + +## Supported Versions + +This is a personal homelab configuration repository. The latest commit on `main` is always the supported version. + +| Branch | Supported | +| ------ | ------------------ | +| main | :white_check_mark: | +| other | :x: | + +## Security Considerations + +### Secrets Management + +**DO NOT commit secrets to this repository!** + +- All passwords in `.env` files should use placeholder values (e.g., `changeme_*`) +- Real passwords should only be set in your local deployment +- Use environment variables or Docker secrets for sensitive data +- Never commit files containing real credentials + +### Container Security + +- All container images are scanned for vulnerabilities via GitHub Actions +- HIGH and CRITICAL vulnerabilities are reported in security scans +- Keep images up to date by pulling latest versions regularly +- Review security scan results before deploying + +### Network Security + +- All services are behind Traefik reverse proxy +- SSL/TLS is enforced via Let's Encrypt +- Internal services use isolated Docker networks +- SSO is enabled on most services via Tinyauth + +### Authentication + +- LLDAP provides centralized user management +- Tinyauth handles SSO authentication +- Services with built-in authentication are documented in README +- Change all default passwords before deployment + +## Reporting a Vulnerability + +If you discover a security vulnerability in this configuration: + +1. **DO NOT** open a public issue +2. Contact the repository owner directly via GitHub private message +3. Include: + - Description of the vulnerability + - Steps to reproduce + - Potential impact + - Suggested fix (if any) + +### What to Report + +- Exposed secrets or credentials +- Insecure configurations +- Vulnerable container images (not already detected by CI) +- Authentication bypasses +- Network security issues + +### What NOT to Report + +- Issues with third-party services (report to their maintainers) +- Theoretical vulnerabilities without proof of concept +- Social engineering attempts + +## Security Best Practices + +### Before Deployment + +1. **Change all passwords** in `.env` files +2. **Review** all service configurations +3. **Update** container images to latest versions +4. **Configure** firewall to only allow ports 80/443 +5. **Enable** automatic security updates on host OS + +### After Deployment + +1. **Monitor** logs regularly for suspicious activity +2. **Update** services monthly (at minimum) +3. **Backup** data regularly +4. **Review** access logs +5. **Test** disaster recovery procedures + +### Network Hardening + +- Use a firewall (ufw, iptables, etc.) +- Only expose ports 80 and 443 to the internet +- Consider using a VPN for administrative access +- Enable fail2ban or similar intrusion prevention +- Use strong DNS providers with DNSSEC + +### Container Hardening + +- Run containers as non-root when possible +- Use read-only filesystems where applicable +- Limit container resources (CPU, memory) +- Enable security options (no-new-privileges, etc.) +- Regularly scan for vulnerabilities + +## Automated Security Scanning + +This repository includes automated security scanning: + +- **Gitleaks**: Detects secrets in commits +- **Trivy**: Scans container images for vulnerabilities +- **YAML Linting**: Ensures proper configuration +- **Dependency Review**: Checks for vulnerable dependencies + +Review GitHub Actions results before merging PRs. + +## Compliance + +This is a personal homelab configuration and does not claim compliance with any specific security standards. However, it follows general security best practices: + +- Principle of least privilege +- Defense in depth +- Secure by default +- Regular updates and patching + +## External Dependencies + +Security of this setup depends on: + +- Docker and Docker Compose security +- Container image maintainers +- Traefik security +- LLDAP security +- Host OS security + +Always keep these dependencies up to date. + +## Disclaimer + +This configuration is provided "as is" without warranty. Use at your own risk. The maintainer is not responsible for any security incidents resulting from the use of this configuration. + +## Additional Resources + +- [Docker Security Best Practices](https://docs.docker.com/engine/security/) +- [Traefik Security Documentation](https://doc.traefik.io/traefik/https/overview/) +- [OWASP Container Security](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html)