diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 07d0b8d..a426210 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,9 +16,13 @@ # Bitwarden lite .github/workflows/build-bitwarden-lite.yml @bitwarden/dept-shot bitwarden-lite/ @bitwarden/dept-shot +CommonMarketplaceLite/ @bitwarden/dept-shot # Release workflows .github/workflows/release-aws.yml @bitwarden/dept-bre +.github/workflows/release-aws-lite.yml @bitwarden/dept-bre .github/workflows/release-azure.yml @bitwarden/dept-bre +.github/workflows/release-azure-lite.yml @bitwarden/dept-bre .github/workflows/release-digital-ocean.yml @bitwarden/dept-bre +.github/workflows/release-digital-ocean-lite.yml @bitwarden/dept-bre .github/workflows/release.yml @bitwarden/dept-bre diff --git a/.github/workflows/release-aws-lite.yml b/.github/workflows/release-aws-lite.yml new file mode 100644 index 0000000..dd75edb --- /dev/null +++ b/.github/workflows/release-aws-lite.yml @@ -0,0 +1,182 @@ +name: Release AWS Marketplace - Bitwarden Lite + +on: + push: + paths: + - "AWSMarketplace/marketplace-image-lite.pkr.hcl" + - "AWSMarketplace/scripts/99-img-check-lite.sh" + - "CommonMarketplaceLite/**" + + workflow_dispatch: + inputs: + release_version: + description: "Release version (e.g., 2026.3.2)" + required: false + type: string + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build-image: + name: Build Image + runs-on: ubuntu-24.04 + timeout-minutes: 90 + permissions: + contents: read + id-token: write + steps: + - name: Checkout repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "gh-self-host" + secrets: "aws-marketplace-access-key-id, + aws-marketplace-secret-access-key" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Set version + id: set-version + env: + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + VERSION=$(jq -r '.versions.coreVersion' version.json) + if [[ -z "$VERSION" ]]; then + echo "ERROR: Failed to extract coreVersion from version.json" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # Use the release version input when dispatched from the release + # pipeline. Fall back to coreVersion for other builds. + if [[ -n "$RELEASE_VERSION" ]]; then + echo "img_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" + else + echo "img_version=$VERSION" >> "$GITHUB_OUTPUT" + fi + + - name: Set up Hashicorp Packer + uses: hashicorp/setup-packer@1aa358be5cf73883762b302a3a03abd66e75b232 # v3.1.0 + + - name: Build AWS Lite Image + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-marketplace-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-marketplace-secret-access-key }} + AWS_DEFAULT_REGION: "us-east-1" + AWS_IMG_VERSION: ${{ steps.set-version.outputs.img_version }} + working-directory: ./AWSMarketplace + run: | + packer version + packer init -upgrade marketplace-image-lite.pkr.hcl + packer build marketplace-image-lite.pkr.hcl + + - name: Cleanup orphaned instances on cancellation or failure + if: cancelled() || failure() + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-marketplace-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-marketplace-secret-access-key }} + AWS_DEFAULT_REGION: "us-east-1" + run: | + echo "Workflow cancelled - cleaning up any orphaned resources..." + + echo "## :warning: Workflow Cancelled - Resource Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Searching for orphaned instances with tag: \`github-run-${{ github.run_id }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Find and terminate EC2 instances tagged with this run + INSTANCE_IDS=$(aws ec2 describe-instances \ + --filters "Name=tag:GitHub_Run,Values=github-run-${{ github.run_id }}" \ + "Name=instance-state-name,Values=pending,running,stopping,stopped" \ + --query "Reservations[].Instances[].InstanceId" \ + --output text) + + if [ -n "$INSTANCE_IDS" ]; then + echo "Found orphaned instances: $INSTANCE_IDS" + echo "### Orphaned Instances Found and Terminated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Instance ID | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------------|--------|" >> $GITHUB_STEP_SUMMARY + + for INSTANCE_ID in $INSTANCE_IDS; do + echo "Terminating instance: $INSTANCE_ID" + if aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"; then + echo "| $INSTANCE_ID | :white_check_mark: Terminated |" >> $GITHUB_STEP_SUMMARY + else + echo "| $INSTANCE_ID | :x: Failed to terminate |" >> $GITHUB_STEP_SUMMARY + fi + done + else + echo "No orphaned instances found" + echo ":white_check_mark: No orphaned resources found - nothing to clean up" >> $GITHUB_STEP_SUMMARY + fi + + - name: Add build summary + if: success() + env: + VERSION: ${{ steps.set-version.outputs.version }} + working-directory: ./AWSMarketplace + run: | + echo "## :rocket: AWS Marketplace Lite Image Build Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Get artifact details from manifest + if [ -f manifest-lite.json ]; then + AMI_ID=$(jq -r '.builds[-1].artifact_id' manifest-lite.json) + echo "**AMI ID:** \`$AMI_ID\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo ":white_check_mark: Packer build instance was automatically cleaned up" >> $GITHUB_STEP_SUMMARY + + - name: AWS Lite Image Cleanup + working-directory: ./AWSMarketplace + if: ${{ inputs.release_version == '' }} + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-marketplace-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-marketplace-secret-access-key }} + AWS_DEFAULT_REGION: "us-east-1" + run: | + # Get the AMI ID from the manifest (format: "us-east-1:ami-xxxxxxxxx") + AMI_ID=$(jq -r '.builds[-1].artifact_id' manifest-lite.json | cut -d ":" -f2) + + if [ -n "$AMI_ID" ]; then + # Find associated snapshots before deregistering + SNAPSHOT_IDS=$(aws ec2 describe-images --image-ids "$AMI_ID" \ + --query "Images[].BlockDeviceMappings[].Ebs.SnapshotId" \ + --output text 2>/dev/null || true) + + # Deregister the AMI + aws ec2 deregister-image --image-id "$AMI_ID" 2>/dev/null || true + + # Delete associated snapshots + for SNAPSHOT_ID in $SNAPSHOT_IDS; do + aws ec2 delete-snapshot --snapshot-id "$SNAPSHOT_ID" 2>/dev/null || true + done + fi + + # Update summary for non-release builds + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo ":wastebasket: **Non-release build:** AMI \`$AMI_ID\` and snapshots were automatically cleaned up" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-azure-lite.yml b/.github/workflows/release-azure-lite.yml new file mode 100644 index 0000000..ea9461c --- /dev/null +++ b/.github/workflows/release-azure-lite.yml @@ -0,0 +1,180 @@ +name: Release Azure Marketplace - Bitwarden Lite + +on: + push: + paths: + - "AzureMarketplace/marketplace-image-lite.pkr.hcl" + - "AzureMarketplace/scripts/99-img-check-lite.sh" + - "CommonMarketplaceLite/**" + + workflow_dispatch: + inputs: + release_version: + description: "Release version (e.g., 2026.3.2)" + required: false + type: string + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build-image: + name: Build Image + runs-on: ubuntu-24.04 + timeout-minutes: 90 + permissions: + contents: read + id-token: write + steps: + - name: Checkout repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "gh-self-host" + secrets: "azure-marketplace-subscription-id" + + - name: Set version + id: set-version + env: + RELEASE_VERSION: ${{ inputs.release_version }} + RUN_NUMBER: ${{ github.run_number }} + run: | + VERSION=$(jq -r '.versions.coreVersion' version.json) + if [[ -z "$VERSION" ]]; then + echo "ERROR: Failed to extract coreVersion from version.json" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # Use the release version input when dispatched from the release + # pipeline. Fall back to a dev version for push builds to avoid + # colliding with published gallery image versions. + if [[ -n "$RELEASE_VERSION" ]]; then + echo "img_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" + else + echo "img_version=0.0.$RUN_NUMBER" >> "$GITHUB_OUTPUT" + fi + + - name: Set up Hashicorp Packer + uses: hashicorp/setup-packer@1aa358be5cf73883762b302a3a03abd66e75b232 # v3.1.0 + + - name: Build Azure Lite Image + env: + AZURE_SUBSCRIPTION_ID: ${{ steps.retrieve-secrets.outputs.azure-marketplace-subscription-id }} + AZURE_RESOURCE_GROUP: rg-marketplace + AZURE_GALLERY_NAME: bitwarden_marketplace + AZURE_GALLERY_IMAGE_NAME: "bitwarden-lite-self-host" + AZURE_IMG_VERSION: ${{ steps.set-version.outputs.img_version }} + working-directory: ./AzureMarketplace + run: | + packer version + packer init -upgrade marketplace-image-lite.pkr.hcl + packer build marketplace-image-lite.pkr.hcl + + - name: Cleanup orphaned resources on cancellation or failure + if: cancelled() || failure() + env: + RESOURCE_GROUP: rg-marketplace + run: | + echo "Workflow cancelled - cleaning up any orphaned resources..." + + echo "## :warning: Workflow Cancelled - Resource Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Searching for orphaned resources with tag: \`github-run-${{ github.run_id }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Find and delete VMs tagged with this run + VM_IDS=$(az vm list \ + --resource-group "$RESOURCE_GROUP" \ + --query "[?tags.github_run=='github-run-${{ github.run_id }}'].id" \ + --output tsv) + + if [ -n "$VM_IDS" ]; then + echo "Found orphaned VMs, deleting..." + echo "### Orphaned VMs Found and Deleted" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + for VM_ID in $VM_IDS; do + echo "Deleting VM: $VM_ID" + if az vm delete --ids "$VM_ID" --yes --force-deletion true; then + echo "| $VM_ID | :white_check_mark: Deleted |" >> $GITHUB_STEP_SUMMARY + else + echo "| $VM_ID | :x: Failed to delete |" >> $GITHUB_STEP_SUMMARY + fi + done + else + echo "No orphaned VMs found" + echo ":white_check_mark: No orphaned resources found - nothing to clean up" >> $GITHUB_STEP_SUMMARY + fi + + - name: Add build summary + if: success() + env: + VERSION: ${{ steps.set-version.outputs.version }} + working-directory: ./AzureMarketplace + run: | + echo "## :rocket: Azure Marketplace Lite Image Build Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Get artifact details from manifest + if [ -f manifest-lite.json ]; then + IMAGE_ID=$(jq -r '.builds[-1].artifact_id' manifest-lite.json) + echo "**Image ID:** \`$IMAGE_ID\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo ":white_check_mark: Packer build VM was automatically cleaned up" >> $GITHUB_STEP_SUMMARY + + - name: Azure Lite Image Cleanup + working-directory: ./AzureMarketplace + if: ${{ inputs.release_version == '' }} + env: + RESOURCE_GROUP: rg-marketplace + GALLERY_NAME: bitwarden_marketplace + VERSION: ${{ steps.set-version.outputs.img_version }} + run: | + # Get the managed image name from manifest + IMAGE_NAME=$(jq -r '.builds[-1].custom_data.managed_image_name // empty' manifest-lite.json) + + # Delete the gallery image version + az sig image-version delete \ + --resource-group "$RESOURCE_GROUP" \ + --gallery-name "$GALLERY_NAME" \ + --gallery-image-definition "bitwarden-lite-self-host" \ + --gallery-image-version "$VERSION" \ + 2>/dev/null || true + + # Delete the managed image + if [ -n "$IMAGE_NAME" ]; then + az image delete \ + --resource-group "$RESOURCE_GROUP" \ + --name "$IMAGE_NAME" \ + 2>/dev/null || true + fi + + # Update summary for non-release builds + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo ":wastebasket: **Non-release build:** Image was automatically cleaned up" >> $GITHUB_STEP_SUMMARY + + - name: Log out from Azure + if: always() + uses: bitwarden/gh-actions/azure-logout@main diff --git a/.github/workflows/release-digital-ocean-lite.yml b/.github/workflows/release-digital-ocean-lite.yml new file mode 100644 index 0000000..eef46e7 --- /dev/null +++ b/.github/workflows/release-digital-ocean-lite.yml @@ -0,0 +1,169 @@ +name: Release Digital Ocean 1-Click - Bitwarden Lite + +on: + push: + paths: + - "DigitalOceanMarketplace/marketplace-image-lite.pkr.hcl" + - "DigitalOceanMarketplace/scripts/99-img-check-lite.sh" + - "CommonMarketplaceLite/**" + + workflow_dispatch: + inputs: + release_version: + description: "Release version (e.g., 2026.3.2)" + required: false + type: string + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build-image: + name: Build Image + runs-on: ubuntu-24.04 + timeout-minutes: 90 + permissions: + contents: read + id-token: write + steps: + - name: Checkout repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "digital-ocean-api-key" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Set version + id: set-version + env: + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + VERSION=$(jq -r '.versions.coreVersion' version.json) + if [[ -z "$VERSION" ]]; then + echo "ERROR: Failed to extract coreVersion from version.json" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # Use the release version input when dispatched from the release + # pipeline. Fall back to coreVersion for other builds. + if [[ -n "$RELEASE_VERSION" ]]; then + echo "img_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT" + else + echo "img_version=$VERSION" >> "$GITHUB_OUTPUT" + fi + + - name: Set up Hashicorp Packer + uses: hashicorp/setup-packer@1aa358be5cf73883762b302a3a03abd66e75b232 # v3.1.0 + + - name: Build Digital Ocean Lite Image + env: + DIGITALOCEAN_TOKEN: ${{ steps.retrieve-secrets.outputs.digital-ocean-api-key }} + DIGITALOCEAN_IMG_VERSION: ${{ steps.set-version.outputs.img_version }} + working-directory: ./DigitalOceanMarketplace + run: | + packer version + packer init -upgrade marketplace-image-lite.pkr.hcl + packer build marketplace-image-lite.pkr.hcl + + - name: Install doctl + uses: digitalocean/action-doctl@135ac0aa0eed4437d547c6f12c364d3006b42824 # v2.5.1 + with: + token: ${{ steps.retrieve-secrets.outputs.digital-ocean-api-key }} + + - name: Cleanup orphaned droplets on cancellation + if: cancelled() + run: | + echo "Workflow cancelled - cleaning up any orphaned droplets..." + + # Write to workflow summary + echo "## :warning: Workflow Cancelled - Droplet Cleanup" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Searching for orphaned droplets with tag: \`github-run-${{ github.run_id }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Find droplets with our workflow run tag + DROPLET_IDS=$(doctl compute droplet list \ + --tag-name "github-run-${{ github.run_id }}" \ + --format ID \ + --no-header) + + if [ -n "$DROPLET_IDS" ]; then + echo "Found orphaned droplets: $DROPLET_IDS" + echo "### Orphaned Droplets Found and Deleted" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Droplet ID | Status |" >> $GITHUB_STEP_SUMMARY + echo "|------------|--------|" >> $GITHUB_STEP_SUMMARY + + for DROPLET_ID in $DROPLET_IDS; do + echo "Deleting droplet $DROPLET_ID..." + if doctl compute droplet delete "$DROPLET_ID" --force; then + echo "| $DROPLET_ID | :white_check_mark: Deleted |" >> $GITHUB_STEP_SUMMARY + else + echo "| $DROPLET_ID | :x: Failed to delete |" >> $GITHUB_STEP_SUMMARY + fi + done + echo "" >> $GITHUB_STEP_SUMMARY + echo ":white_check_mark: Cleanup complete" >> $GITHUB_STEP_SUMMARY + else + echo "No orphaned droplets found" + echo ":white_check_mark: No orphaned droplets found - nothing to clean up" >> $GITHUB_STEP_SUMMARY + fi + + - name: Add build summary + if: success() + env: + VERSION: ${{ steps.set-version.outputs.version }} + working-directory: ./DigitalOceanMarketplace + run: | + echo "## :rocket: Digital Ocean Lite Image Build Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Get artifact details from manifest + if [ -f manifest-lite.json ]; then + SNAPSHOT_ID=$(jq -r '.builds[-1].artifact_id' manifest-lite.json | cut -d ":" -f2) + SNAPSHOT_NAME=$(jq -r '.builds[-1].custom_data.snapshot_name // "N/A"' manifest-lite.json) + echo "**Snapshot ID:** \`$SNAPSHOT_ID\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Snapshot Name:** \`$SNAPSHOT_NAME\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo ":white_check_mark: Packer build droplet was automatically cleaned up" >> $GITHUB_STEP_SUMMARY + + - name: Digital Ocean Lite Image Cleanup + working-directory: ./DigitalOceanMarketplace + if: ${{ inputs.release_version == '' }} + run: | + # Get the ID from the snapshot build. + DO_ARTIFACT=$(jq -r '.builds[-1].artifact_id' manifest-lite.json | cut -d ":" -f2) + + # Force remove the snapshot + doctl compute image delete "$DO_ARTIFACT" -f + + # Update summary for non-release builds + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo ":wastebasket: **Non-release build:** Snapshot \`$DO_ARTIFACT\` was automatically cleaned up" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3d45df..e5217a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -538,3 +538,51 @@ jobs: ref: process.env.RELEASE_TAG, inputs: { release_version: process.env.RELEASE_VERSION } }); + + - name: Trigger release-digital-ocean-lite workflow + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + RELEASE_TAG: v${{ inputs.release_version }} + RELEASE_VERSION: ${{ inputs.release_version }} + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'self-host', + workflow_id: 'release-digital-ocean-lite.yml', + ref: process.env.RELEASE_TAG, + inputs: { release_version: process.env.RELEASE_VERSION } + }); + + - name: Trigger release-aws-lite workflow + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + RELEASE_TAG: v${{ inputs.release_version }} + RELEASE_VERSION: ${{ inputs.release_version }} + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'self-host', + workflow_id: 'release-aws-lite.yml', + ref: process.env.RELEASE_TAG, + inputs: { release_version: process.env.RELEASE_VERSION } + }); + + - name: Trigger release-azure-lite workflow + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + RELEASE_TAG: v${{ inputs.release_version }} + RELEASE_VERSION: ${{ inputs.release_version }} + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'self-host', + workflow_id: 'release-azure-lite.yml', + ref: process.env.RELEASE_TAG, + inputs: { release_version: process.env.RELEASE_VERSION } + }); diff --git a/AWSMarketplace/marketplace-image-lite.pkr.hcl b/AWSMarketplace/marketplace-image-lite.pkr.hcl new file mode 100644 index 0000000..eac9c3d --- /dev/null +++ b/AWSMarketplace/marketplace-image-lite.pkr.hcl @@ -0,0 +1,164 @@ +packer { + required_plugins { + amazon = { + version = ">= 1.2.0" + source = "github.com/hashicorp/amazon" + } + } +} + +variable "application_name" { + type = string + default = "Bitwarden Lite" +} + +variable "application_version" { + type = string + default = "${env("AWS_IMG_VERSION")}" +} + +variable "apt_packages" { + type = string + default = "fail2ban ca-certificates curl gnupg" +} + +variable "docker_packages" { + type = string + default = "docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" +} + +variable "aws_region" { + type = string + default = "us-east-1" +} + +variable "github_run_id" { + type = string + default = "${env("GITHUB_RUN_ID")}" +} + +# "timestamp" template function replacement +locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } + +locals { + image_name = "bitwarden-lite-24-04-${local.timestamp}" +} + +source "amazon-ebs" "bitwarden_lite" { + region = var.aws_region + instance_type = "t3.small" + ssh_username = "ubuntu" + + source_ami_filter { + filters = { + name = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*" + root-device-type = "ebs" + virtualization-type = "hvm" + } + most_recent = true + owners = ["099720109477"] # Canonical + } + + launch_block_device_mappings { + device_name = "/dev/sda1" + volume_size = 32 + volume_type = "gp3" + delete_on_termination = true + } + + ami_name = local.image_name + ami_description = "Bitwarden Lite Self-Host ${var.application_version}" + + tags = { + Name = local.image_name + Application = "bitwarden-packer-build" + Version = var.application_version + GitHub_Run = "github-run-${var.github_run_id}" + } + + run_tags = { + Name = "packer-bitwarden-lite-${var.github_run_id}" + Application = "bitwarden-packer-build" + GitHub_Run = "github-run-${var.github_run_id}" + } +} + +build { + sources = ["source.amazon-ebs.bitwarden_lite"] + + provisioner "shell" { + inline = ["cloud-init status --wait"] + } + + # Upload Bitwarden Lite files to /tmp staging area (amazon-ebs connects as a non-root user) + provisioner "file" { + source = "../CommonMarketplaceLite/files/etc/update-motd.d/99-bitwarden-welcome" + destination = "/tmp/99-bitwarden-welcome" + } + + provisioner "file" { + source = "../CommonMarketplace/files/etc/ufw/applications.d/bitwarden" + destination = "/tmp/bitwarden-ufw" + } + + provisioner "file" { + source = "../CommonMarketplaceLite/files/var/lib/cloud/scripts/per-instance/001_onboot" + destination = "/tmp/001_onboot" + } + + # Move staged files to their final system locations + provisioner "shell" { + inline = [ + "sudo mkdir -p /etc/update-motd.d /etc/ufw/applications.d /var/lib/cloud/scripts/per-instance", + "sudo mv /tmp/99-bitwarden-welcome /etc/update-motd.d/99-bitwarden-welcome", + "sudo mv /tmp/bitwarden-ufw /etc/ufw/applications.d/bitwarden", + "sudo mv /tmp/001_onboot /var/lib/cloud/scripts/per-instance/001_onboot", + "sudo chown root:root /etc/update-motd.d/99-bitwarden-welcome /etc/ufw/applications.d/bitwarden /var/lib/cloud/scripts/per-instance/001_onboot", + "sudo chmod 644 /etc/ufw/applications.d/bitwarden" + ] + } + + provisioner "shell" { + environment_vars = [ + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ] + inline = [ + "sudo apt-get -qqy update", + "sudo apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade", + "sudo apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install ${var.apt_packages}", + "sudo install -m 0755 -d /etc/apt/keyrings", + "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg", + "sudo chmod a+r /etc/apt/keyrings/docker.gpg", + "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable\" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null", + "sudo apt-get -qqy update", + "sudo apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install ${var.docker_packages}", + "sudo apt-get -qqy clean" + ] + } + + provisioner "shell" { + execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E bash '{{ .Path }}'" + environment_vars = [ + "application_name=${var.application_name}", + "application_version=${var.application_version}", + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ] + scripts = [ + "../CommonMarketplace/scripts/01-setup-first-run.sh", + "../CommonMarketplace/scripts/02-ufw-bitwarden.sh", + "../CommonMarketplace/scripts/90-cleanup.sh", + "scripts/99-img-check-lite.sh" + ] + } + + post-processor "manifest" { + output = "manifest-lite.json" + strip_path = true + } +} diff --git a/AWSMarketplace/scripts/99-img-check-lite.sh b/AWSMarketplace/scripts/99-img-check-lite.sh new file mode 100644 index 0000000..908fbbd --- /dev/null +++ b/AWSMarketplace/scripts/99-img-check-lite.sh @@ -0,0 +1,191 @@ +#!/bin/bash + +# AWS Marketplace Image Validation Tool - Bitwarden Lite + +VERSION="v. 1.0.0" +RUNDATE=$( date ) + +# Script should be run with SUDO +if [ "$EUID" -ne 0 ] + then echo "[Error] - This script must be run with sudo or as the root user." + exit 1 +fi + +STATUS=0 +PASS=0 +WARN=0 +FAIL=0 + +clear +echo "AWS Marketplace Image Validation Tool (Bitwarden Lite) ${VERSION}" +echo "Executed on: ${RUNDATE}" +echo "Checking local system for Marketplace compatibility..." +echo "" + +# Check OS +if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME + VER=$VERSION_ID +else + OS=$(uname -s) + VER=$(uname -r) +fi + +echo -en "Distribution: ${OS}\n" +echo -en "Version: ${VER}\n\n" + +if [[ $OS == "Ubuntu" ]] && [[ $VER == "24.04" ]]; then + echo -en "\e[32m[PASS]\e[0m Supported OS detected: ${OS} ${VER}\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not the expected OS (Ubuntu 24.04)\n" + ((FAIL++)) + STATUS=2 +fi + +# Check cloud-init +if hash cloud-init 2>/dev/null; then + echo -en "\e[32m[PASS]\e[0m Cloud-init is installed.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Cloud-init is not installed.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check Docker +if hash docker 2>/dev/null; then + echo -en "\e[32m[PASS]\e[0m Docker is installed.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Docker is not installed.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check docker compose plugin +if docker compose version > /dev/null 2>&1; then + echo -en "\e[32m[PASS]\e[0m Docker Compose plugin is installed.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Docker Compose plugin is not installed.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check firewall +if [[ $OS == "Ubuntu" ]]; then + ufwa=$(ufw status | head -1 | sed -e "s/^Status:\ //") + if [[ $ufwa == "active" ]]; then + echo -en "\e[32m[PASS]\e[0m Firewall (ufw) is active.\n" + ((PASS++)) + else + echo -en "\e[93m[WARN]\e[0m Firewall (ufw) is not active.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi + fi +fi + +# Check root password +SHADOW=$(cat /etc/shadow) +for usr in $SHADOW; do + IFS=':' read -r -a u <<< "$usr" + if [[ "${u[0]}" == "root" ]]; then + if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then + echo -en "\e[32m[PASS]\e[0m Root user has no password set.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m Root user has a password set.\n" + ((FAIL++)) + STATUS=2 + fi + fi +done + +# Check SSH keys +if [ -f /root/.ssh/authorized_keys ] && [ "$(wc -c < /root/.ssh/authorized_keys)" -gt 50 ]; then + echo -en "\e[41m[FAIL]\e[0m Root has a populated authorized_keys file.\n" + ((FAIL++)) + STATUS=2 +else + echo -en "\e[32m[PASS]\e[0m No SSH keys found for root.\n" + ((PASS++)) +fi + +if [ -f /home/ubuntu/.ssh/authorized_keys ] && [ "$(wc -c < /home/ubuntu/.ssh/authorized_keys)" -gt 50 ]; then + echo -en "\e[41m[FAIL]\e[0m Ubuntu user has a populated authorized_keys file.\n" + ((FAIL++)) + STATUS=2 +else + echo -en "\e[32m[PASS]\e[0m No SSH keys found for ubuntu user.\n" + ((PASS++)) +fi + +# Check bash history +if [ -f /root/.bash_history ]; then + BH_S=$(wc -c < /root/.bash_history) + if [[ $BH_S -lt 200 ]]; then + echo -en "\e[32m[PASS]\e[0m Root bash history appears cleared.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m Root bash history should be cleared.\n" + ((FAIL++)) + STATUS=2 + fi +else + echo -en "\e[32m[PASS]\e[0m Root bash history is not present.\n" + ((PASS++)) +fi + +# Check cloud-init first-boot script is present and executable +if [ -x /var/lib/cloud/scripts/per-instance/001_onboot ]; then + echo -en "\e[32m[PASS]\e[0m Cloud-init first-boot script is present and executable.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Cloud-init first-boot script not found at /var/lib/cloud/scripts/per-instance/001_onboot.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check for log files +echo -en "\nChecking for log files in /var/log\n" +for f in /var/log/*-????????; do + [[ -e $f ]] || break + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi +done +for f in /var/log/*.[0-9]; do + [[ -e $f ]] || break + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi +done + +# Summary +echo -en "\n---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n" +elif [[ $STATUS == 1 ]]; then + echo -en "Scan Complete.\n\e[93mSome non-critical tests failed. Please review these items.\e[0m\n" +else + echo -en "Scan Complete.\n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n" +fi +echo "---------------------------------------------------------------------------------------------------" +echo -en "\e[1m${PASS} Tests PASSED\e[0m\n" +echo -en "\e[1m${WARN} WARNINGS\e[0m\n" +echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n" +echo -en "---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "No issues detected. Ensure all software is functional, secure, and properly configured.\n\n" + exit 0 +elif [[ $STATUS == 1 ]]; then + echo -en "Please review all [WARN] items above and ensure they are intended or resolved.\n\n" + exit 0 +else + echo -en "Critical tests failed. These must be resolved before submitting to AWS Marketplace.\n\n" + exit 1 +fi diff --git a/AzureMarketplace/marketplace-image-lite.pkr.hcl b/AzureMarketplace/marketplace-image-lite.pkr.hcl new file mode 100644 index 0000000..db5bde9 --- /dev/null +++ b/AzureMarketplace/marketplace-image-lite.pkr.hcl @@ -0,0 +1,191 @@ +packer { + required_plugins { + azure = { + version = ">= 2.0.0" + source = "github.com/hashicorp/azure" + } + } +} + +variable "application_name" { + type = string + default = "Bitwarden Lite" +} + +variable "application_version" { + type = string + default = "${env("AZURE_IMG_VERSION")}" +} + +variable "apt_packages" { + type = string + default = "fail2ban ca-certificates curl gnupg" +} + +variable "docker_packages" { + type = string + default = "docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" +} + +variable "subscription_id" { + type = string + default = "${env("AZURE_SUBSCRIPTION_ID")}" +} + +variable "resource_group" { + type = string + default = "${env("AZURE_RESOURCE_GROUP")}" +} + +variable "gallery_name" { + type = string + default = "${env("AZURE_GALLERY_NAME")}" +} + +variable "gallery_image_name" { + type = string + default = "${env("AZURE_GALLERY_IMAGE_NAME")}" +} + +variable "location" { + type = string + default = "East US" +} + +variable "github_run_id" { + type = string + default = "${env("GITHUB_RUN_ID")}" +} + +# "timestamp" template function replacement +locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } + +locals { + image_name = "bitwarden-lite-24-04-${local.timestamp}" +} + +source "azure-arm" "bitwarden_lite" { + use_azure_cli_auth = true + subscription_id = var.subscription_id + + os_type = "Linux" + image_publisher = "Canonical" + image_offer = "ubuntu-24_04-lts" + image_sku = "server" + + build_resource_group_name = var.resource_group + vm_size = "Standard_B2s" + + managed_image_name = local.image_name + managed_image_resource_group_name = var.resource_group + + shared_image_gallery_destination { + subscription = var.subscription_id + resource_group = var.resource_group + gallery_name = var.gallery_name + image_name = var.gallery_image_name + image_version = var.application_version + replication_regions = [var.location] + } + + azure_tags = { + application = "bitwarden-packer-build" + github_run = "github-run-${var.github_run_id}" + } +} + +build { + sources = ["source.azure-arm.bitwarden_lite"] + + provisioner "shell" { + inline = ["cloud-init status --wait"] + } + + # Upload Bitwarden Lite files to /tmp staging area (azure-arm connects as a non-root user) + provisioner "file" { + source = "../CommonMarketplaceLite/files/etc/update-motd.d/99-bitwarden-welcome" + destination = "/tmp/99-bitwarden-welcome" + } + + provisioner "file" { + source = "../CommonMarketplace/files/etc/ufw/applications.d/bitwarden" + destination = "/tmp/bitwarden-ufw" + } + + provisioner "file" { + source = "../CommonMarketplaceLite/files/var/lib/cloud/scripts/per-instance/001_onboot" + destination = "/tmp/001_onboot" + } + + # Move staged files to their final system locations + provisioner "shell" { + inline = [ + "sudo mkdir -p /etc/update-motd.d /etc/ufw/applications.d /var/lib/cloud/scripts/per-instance", + "sudo mv /tmp/99-bitwarden-welcome /etc/update-motd.d/99-bitwarden-welcome", + "sudo mv /tmp/bitwarden-ufw /etc/ufw/applications.d/bitwarden", + "sudo mv /tmp/001_onboot /var/lib/cloud/scripts/per-instance/001_onboot", + "sudo chown root:root /etc/update-motd.d/99-bitwarden-welcome /etc/ufw/applications.d/bitwarden /var/lib/cloud/scripts/per-instance/001_onboot", + "sudo chmod 644 /etc/ufw/applications.d/bitwarden" + ] + } + + provisioner "shell" { + environment_vars = [ + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ] + inline = [ + "sudo apt-get -qqy update", + "sudo apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade", + "sudo apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install ${var.apt_packages}", + "sudo install -m 0755 -d /etc/apt/keyrings", + "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg", + "sudo chmod a+r /etc/apt/keyrings/docker.gpg", + "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable\" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null", + "sudo apt-get -qqy update", + "sudo apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install ${var.docker_packages}", + "sudo apt-get -qqy clean" + ] + } + + provisioner "shell" { + execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E bash '{{ .Path }}'" + environment_vars = [ + "application_name=${var.application_name}", + "application_version=${var.application_version}", + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ] + scripts = [ + "../CommonMarketplace/scripts/01-setup-first-run.sh", + "../CommonMarketplace/scripts/02-ufw-bitwarden.sh", + "../CommonMarketplace/scripts/90-cleanup.sh", + "scripts/99-img-check-lite.sh" + ] + } + + # Azure-specific cleanup + provisioner "shell" { + execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E bash '{{ .Path }}'" + inline = [ + "truncate -s 0 /var/log/waagent.log 2>/dev/null || true" + ] + } + + # Azure generalization - must be the last provisioner + provisioner "shell" { + execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'" + inline = [ + "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync" + ] + } + + post-processor "manifest" { + output = "manifest-lite.json" + strip_path = true + } +} diff --git a/AzureMarketplace/scripts/99-img-check-lite.sh b/AzureMarketplace/scripts/99-img-check-lite.sh new file mode 100644 index 0000000..2606d72 --- /dev/null +++ b/AzureMarketplace/scripts/99-img-check-lite.sh @@ -0,0 +1,201 @@ +#!/bin/bash + +# Azure Marketplace Image Validation Tool - Bitwarden Lite + +VERSION="v. 1.0.0" +RUNDATE=$( date ) + +# Script should be run with SUDO +if [ "$EUID" -ne 0 ] + then echo "[Error] - This script must be run with sudo or as the root user." + exit 1 +fi + +STATUS=0 +PASS=0 +WARN=0 +FAIL=0 + +clear +echo "Azure Marketplace Image Validation Tool (Bitwarden Lite) ${VERSION}" +echo "Executed on: ${RUNDATE}" +echo "Checking local system for Marketplace compatibility..." +echo "" + +# Check OS +if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME + VER=$VERSION_ID +else + OS=$(uname -s) + VER=$(uname -r) +fi + +echo -en "Distribution: ${OS}\n" +echo -en "Version: ${VER}\n\n" + +if [[ $OS == "Ubuntu" ]] && [[ $VER == "24.04" ]]; then + echo -en "\e[32m[PASS]\e[0m Supported OS detected: ${OS} ${VER}\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not the expected OS (Ubuntu 24.04)\n" + ((FAIL++)) + STATUS=2 +fi + +# Check cloud-init +if hash cloud-init 2>/dev/null; then + echo -en "\e[32m[PASS]\e[0m Cloud-init is installed.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Cloud-init is not installed.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check Azure Linux Agent +if hash waagent 2>/dev/null; then + echo -en "\e[32m[PASS]\e[0m Azure Linux Agent (waagent) is installed.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Azure Linux Agent (waagent) is not installed.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check Docker +if hash docker 2>/dev/null; then + echo -en "\e[32m[PASS]\e[0m Docker is installed.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Docker is not installed.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check docker compose plugin +if docker compose version > /dev/null 2>&1; then + echo -en "\e[32m[PASS]\e[0m Docker Compose plugin is installed.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Docker Compose plugin is not installed.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check firewall +if [[ $OS == "Ubuntu" ]]; then + ufwa=$(ufw status | head -1 | sed -e "s/^Status:\ //") + if [[ $ufwa == "active" ]]; then + echo -en "\e[32m[PASS]\e[0m Firewall (ufw) is active.\n" + ((PASS++)) + else + echo -en "\e[93m[WARN]\e[0m Firewall (ufw) is not active.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi + fi +fi + +# Check root password +SHADOW=$(cat /etc/shadow) +for usr in $SHADOW; do + IFS=':' read -r -a u <<< "$usr" + if [[ "${u[0]}" == "root" ]]; then + if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then + echo -en "\e[32m[PASS]\e[0m Root user has no password set.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m Root user has a password set.\n" + ((FAIL++)) + STATUS=2 + fi + fi +done + +# Check SSH keys +if [ -f /root/.ssh/authorized_keys ] && [ "$(wc -c < /root/.ssh/authorized_keys)" -gt 50 ]; then + echo -en "\e[41m[FAIL]\e[0m Root has a populated authorized_keys file.\n" + ((FAIL++)) + STATUS=2 +else + echo -en "\e[32m[PASS]\e[0m No SSH keys found for root.\n" + ((PASS++)) +fi + +if [ -f /home/ubuntu/.ssh/authorized_keys ] && [ "$(wc -c < /home/ubuntu/.ssh/authorized_keys)" -gt 50 ]; then + echo -en "\e[41m[FAIL]\e[0m Ubuntu user has a populated authorized_keys file.\n" + ((FAIL++)) + STATUS=2 +else + echo -en "\e[32m[PASS]\e[0m No SSH keys found for ubuntu user.\n" + ((PASS++)) +fi + +# Check bash history +if [ -f /root/.bash_history ]; then + BH_S=$(wc -c < /root/.bash_history) + if [[ $BH_S -lt 200 ]]; then + echo -en "\e[32m[PASS]\e[0m Root bash history appears cleared.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m Root bash history should be cleared.\n" + ((FAIL++)) + STATUS=2 + fi +else + echo -en "\e[32m[PASS]\e[0m Root bash history is not present.\n" + ((PASS++)) +fi + +# Check cloud-init first-boot script is present and executable +if [ -x /var/lib/cloud/scripts/per-instance/001_onboot ]; then + echo -en "\e[32m[PASS]\e[0m Cloud-init first-boot script is present and executable.\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m Cloud-init first-boot script not found at /var/lib/cloud/scripts/per-instance/001_onboot.\n" + ((FAIL++)) + STATUS=2 +fi + +# Check for log files +echo -en "\nChecking for log files in /var/log\n" +for f in /var/log/*-????????; do + [[ -e $f ]] || break + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi +done +for f in /var/log/*.[0-9]; do + [[ -e $f ]] || break + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi +done + +# Summary +echo -en "\n---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n" +elif [[ $STATUS == 1 ]]; then + echo -en "Scan Complete.\n\e[93mSome non-critical tests failed. Please review these items.\e[0m\n" +else + echo -en "Scan Complete.\n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n" +fi +echo "---------------------------------------------------------------------------------------------------" +echo -en "\e[1m${PASS} Tests PASSED\e[0m\n" +echo -en "\e[1m${WARN} WARNINGS\e[0m\n" +echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n" +echo -en "---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "No issues detected. Ensure all software is functional, secure, and properly configured.\n\n" + exit 0 +elif [[ $STATUS == 1 ]]; then + echo -en "Please review all [WARN] items above and ensure they are intended or resolved.\n\n" + exit 0 +else + echo -en "Critical tests failed. These must be resolved before submitting to Azure Marketplace.\n\n" + exit 1 +fi diff --git a/CommonMarketplace/scripts/01-setup-first-run.sh b/CommonMarketplace/scripts/01-setup-first-run.sh index 2e07f21..b76d983 100644 --- a/CommonMarketplace/scripts/01-setup-first-run.sh +++ b/CommonMarketplace/scripts/01-setup-first-run.sh @@ -26,4 +26,6 @@ chmod +x /etc/update-motd.d/99-bitwarden-welcome # Setup First Run Script # -chmod +x /opt/bitwarden/install-bitwarden.sh +if [ -f /opt/bitwarden/install-bitwarden.sh ]; then + chmod +x /opt/bitwarden/install-bitwarden.sh +fi diff --git a/CommonMarketplaceLite/files/etc/update-motd.d/99-bitwarden-welcome b/CommonMarketplaceLite/files/etc/update-motd.d/99-bitwarden-welcome new file mode 100644 index 0000000..932d740 --- /dev/null +++ b/CommonMarketplaceLite/files/etc/update-motd.d/99-bitwarden-welcome @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Configured as part of the Bitwarden Lite Marketplace Image build process + +SETTINGS_FILE="/home/bitwarden/settings.env" + +# Read current values from settings.env if it exists +if [ -f "$SETTINGS_FILE" ]; then + BW_DOMAIN=$(grep "^BW_DOMAIN=" "$SETTINGS_FILE" | cut -d'=' -f2-) + BW_INSTALLATION_ID=$(grep "^BW_INSTALLATION_ID=" "$SETTINGS_FILE" | cut -d'=' -f2-) + BW_INSTALLATION_KEY=$(grep "^BW_INSTALLATION_KEY=" "$SETTINGS_FILE" | cut -d'=' -f2-) +else + BW_DOMAIN="(settings.env not found)" + BW_INSTALLATION_ID="" + BW_INSTALLATION_KEY="" +fi + +# Check which required fields still need to be configured +NEEDS_CONFIG=0 +CONFIG_ITEMS="" + +if [ "$BW_DOMAIN" = "bitwarden.example.com" ] || [ -z "$BW_DOMAIN" ]; then + CONFIG_ITEMS="${CONFIG_ITEMS} - BW_DOMAIN is not set (currently: ${BW_DOMAIN})\n" + NEEDS_CONFIG=1 +fi + +if [ "$BW_INSTALLATION_ID" = "00000000-0000-0000-0000-000000000000" ] || [ -z "$BW_INSTALLATION_ID" ]; then + CONFIG_ITEMS="${CONFIG_ITEMS} - BW_INSTALLATION_ID is not set\n" + NEEDS_CONFIG=1 +fi + +if [ "$BW_INSTALLATION_KEY" = "xxxxxxxxxxxx" ] || [ -z "$BW_INSTALLATION_KEY" ]; then + CONFIG_ITEMS="${CONFIG_ITEMS} - BW_INSTALLATION_KEY is not set\n" + NEEDS_CONFIG=1 +fi + +cat < /etc/cron.weekly/bitwardenupdate +chmod +x /etc/cron.weekly/bitwardenupdate diff --git a/DigitalOceanMarketplace/marketplace-image-lite.pkr.hcl b/DigitalOceanMarketplace/marketplace-image-lite.pkr.hcl new file mode 100644 index 0000000..a46cde3 --- /dev/null +++ b/DigitalOceanMarketplace/marketplace-image-lite.pkr.hcl @@ -0,0 +1,139 @@ +packer { + required_plugins { + digitalocean = { + version = ">= 1.0.4" + source = "github.com/digitalocean/digitalocean" + } + } +} + +variable "application_name" { + type = string + default = "Bitwarden Lite" +} + +variable "application_version" { + type = string + default = "${env("DIGITALOCEAN_IMG_VERSION")}" +} + +variable "apt_packages" { + type = string + default = "fail2ban ca-certificates curl gnupg" +} + +variable "docker_packages" { + type = string + default = "docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" +} + +variable "do_token" { + type = string + default = "${env("DIGITALOCEAN_TOKEN")}" + sensitive = true +} + +variable "github_run_id" { + type = string + default = "${env("GITHUB_RUN_ID")}" +} + +# "timestamp" template function replacement +locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } + +locals { + image_name = "bitwarden-lite-24-04-snapshot-${local.timestamp}" +} + +source "digitalocean" "bitwarden_lite" { + api_token = "${var.do_token}" + image = "ubuntu-24-04-x64" + region = "nyc3" + size = "s-1vcpu-2gb" + snapshot_name = "${local.image_name}" + ssh_username = "root" + tags = [ + "bitwarden-packer-build", + "github-run-${var.github_run_id}" + ] +} + +build { + sources = ["source.digitalocean.bitwarden_lite"] + + provisioner "shell" { + inline = ["cloud-init status --wait"] + } + + # Upload Bitwarden Lite files directly (DO connects as root) + provisioner "file" { + destination = "/etc/update-motd.d/99-bitwarden-welcome" + source = "../CommonMarketplaceLite/files/etc/update-motd.d/99-bitwarden-welcome" + } + + provisioner "file" { + destination = "/etc/ufw/applications.d/bitwarden" + source = "../CommonMarketplace/files/etc/ufw/applications.d/bitwarden" + } + + provisioner "file" { + destination = "/var/lib/cloud/scripts/per-instance/001_onboot" + source = "../CommonMarketplaceLite/files/var/lib/cloud/scripts/per-instance/001_onboot" + } + + provisioner "shell" { + environment_vars = [ + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ] + inline = [ + "apt-get -qqy update", + "apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade", + "apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install ${var.apt_packages}", + "install -m 0755 -d /etc/apt/keyrings", + "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg", + "chmod a+r /etc/apt/keyrings/docker.gpg", + "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null", + "apt-get -qqy update", + "apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install ${var.docker_packages}", + "apt-get -qqy clean", + "rm -rf /opt/digitalocean", + "rm -rf /var/log/auth.log", + "rm -rf /var/log/kern.log", + "rm -rf /var/log/ufw.log", + "rm -rf /var/log/ubuntu-advantage.log", + "rm -rf /var/log/droplet-agent.update.log" + ] + } + + provisioner "shell" { + environment_vars = [ + "application_name=${var.application_name}", + "application_version=${var.application_version}", + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ] + scripts = [ + "../CommonMarketplace/scripts/01-setup-first-run.sh", + "../CommonMarketplace/scripts/02-ufw-bitwarden.sh", + "../CommonMarketplace/scripts/90-cleanup.sh", + "scripts/99-img-check-lite.sh" + ] + } + + # DO-specific: securely erase unused disk space for snapshot compression + provisioner "shell" { + inline = [ + "dd if=/dev/zero of=/zerofile bs=4096 || rm /zerofile" + ] + } + + post-processor "manifest" { + output = "manifest-lite.json" + strip_path = true + } +} diff --git a/DigitalOceanMarketplace/scripts/99-img-check-lite.sh b/DigitalOceanMarketplace/scripts/99-img-check-lite.sh new file mode 100644 index 0000000..2667cc1 --- /dev/null +++ b/DigitalOceanMarketplace/scripts/99-img-check-lite.sh @@ -0,0 +1,261 @@ +#!/bin/bash + +# DigitalOcean Marketplace Image Validation Tool - Bitwarden Lite +# © 2021-2022 DigitalOcean LLC. +# This code is licensed under Apache 2.0 license (see LICENSE.md for details) + +VERSION="v. 1.0.0" +RUNDATE=$( date ) + +# Script should be run with SUDO +if [ "$EUID" -ne 0 ] + then echo "[Error] - This script must be run with sudo or as the root user." + exit 1 +fi + +STATUS=0 +PASS=0 +WARN=0 +FAIL=0 + +# $1 == command to check for +# returns: 0 == true, 1 == false +cmdExists() { + if command -v "$1" > /dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +function getDistro { + if [ -f /etc/os-release ]; then + # shellcheck disable=SC1091 + . /etc/os-release + OS=$NAME + VER=$VERSION_ID +elif type lsb_release >/dev/null 2>&1; then + OS=$(lsb_release -si) + VER=$(lsb_release -sr) +elif [ -f /etc/lsb-release ]; then + # shellcheck disable=SC1091 + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE +elif [ -f /etc/debian_version ]; then + OS=Debian + VER=$(cat /etc/debian_version) +else + OS=$(uname -s) + VER=$(uname -r) +fi +} + +function loadPasswords { +SHADOW=$(cat /etc/shadow) +} + +function checkAgent { + if [ -d /opt/digitalocean ];then + echo -en "\e[41m[FAIL]\e[0m DigitalOcean directory detected.\n" + ((FAIL++)) + STATUS=2 + if [[ $OS == "Ubuntu" ]] || [[ $OS == "Debian" ]]; then + echo "To uninstall the agent and remove the DO directory: 'sudo apt-get purge droplet-agent'" + fi + else + echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n" + ((PASS++)) + fi +} + +function checkLogs { + echo -en "\nChecking for log files in /var/log\n\n" + for f in /var/log/*-????????; do + [[ -e $f ]] || break + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi + done + for f in /var/log/*.[0-9]; do + [[ -e $f ]] || break + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi + done + for f in /var/log/*.log; do + [[ -e $f ]] || break + if [[ "$(wc -c < "${f}")" -gt 50 ]]; then + echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found.\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then STATUS=1; fi + fi + done +} + +function checkRoot { + user="root" + uhome="/root" + for usr in $SHADOW + do + IFS=':' read -r -a u <<< "$usr" + if [[ "${u[0]}" == "${user}" ]]; then + if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then + echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n" + ((FAIL++)) + STATUS=2 + fi + fi + done + if [ -d ${uhome}/ ]; then + if [ -d ${uhome}/.ssh/ ]; then + if ls ${uhome}/.ssh/* > /dev/null 2>&1; then + for key in "${uhome}"/.ssh/* + do + if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then + if [ "$(wc -c < "${key}")" -gt 50 ]; then + echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n" + ((FAIL++)) + STATUS=2 + fi + fi + done + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n" + fi + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n" + fi + if [ -f /root/.bash_history ]; then + BH_S=$(wc -c < /root/.bash_history) + if [[ $BH_S -lt 200 ]]; then + echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n" + ((FAIL++)) + STATUS=2 + fi + else + echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n" + ((PASS++)) + fi + fi + echo -en "\n\n" + return 1 +} + +function checkFirewall { + if [[ $OS == "Ubuntu" ]]; then + fw="ufw" + ufwa=$(ufw status | head -1 | sed -e "s/^Status:\ //") + if [[ $ufwa == "active" ]]; then + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + fi +} + +function checkCloudInit { + if hash cloud-init 2>/dev/null; then + CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n" + ((PASS++)) + else + CI="\e[41m[FAIL]\e[0m No valid version of cloud-init was found.\n" + ((FAIL++)) + STATUS=2 + fi + return 1 +} + +function checkBitwardenLite { + if [ -x /var/lib/cloud/scripts/per-instance/001_onboot ]; then + echo -en "\e[32m[PASS]\e[0m Cloud-init first-boot script is present and executable.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m Cloud-init first-boot script not found at /var/lib/cloud/scripts/per-instance/001_onboot.\n" + ((FAIL++)) + STATUS=2 + fi +} + +function checkDockerCompose { + if docker compose version > /dev/null 2>&1; then + echo -en "\e[32m[PASS]\e[0m Docker Compose plugin is installed.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m Docker Compose plugin is not installed.\n" + ((FAIL++)) + STATUS=2 + fi +} + +clear +echo "DigitalOcean Marketplace Image Validation Tool (Bitwarden Lite) ${VERSION}" +echo "Executed on: ${RUNDATE}" +echo "Checking local system for Marketplace compatibility..." + +getDistro + +echo -en "\n\e[1mDistribution:\e[0m ${OS}\n" +echo -en "\e[1mVersion:\e[0m ${VER}\n\n" + +if [[ $OS == "Ubuntu" ]] && [[ $VER == "24.04" ]]; then + echo -en "\e[32m[PASS]\e[0m Supported OS detected: ${OS} ${VER}\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not the expected OS (Ubuntu 24.04)\n" + ((FAIL++)) + STATUS=2 +fi + +checkCloudInit +echo -en "${CI}" + +checkFirewall +echo -en "${FW_VER}" + +checkDockerCompose + +loadPasswords +checkLogs + +echo -en "\n\nChecking the root account...\n" +checkRoot + +checkAgent + +checkBitwardenLite + +# Summary +echo -en "\n\n---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n" +elif [[ $STATUS == 1 ]]; then + echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n" +else + echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n" +fi +echo "---------------------------------------------------------------------------------------------------" +echo -en "\e[1m${PASS} Tests PASSED\e[0m\n" +echo -en "\e[1m${WARN} WARNINGS\e[0m\n" +echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n" +echo -en "---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "We did not detect any issues with this image.\n\n" + exit 0 +elif [[ $STATUS == 1 ]]; then + echo -en "Please review all [WARN] items above and ensure they are intended or resolved.\n\n" + exit 0 +else + echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n" + exit 1 +fi diff --git a/README.md b/README.md index 065f591..6d4e361 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,6 @@ $ cosign verify ghcr.io/bitwarden/$IMAGE_NAME:latest --certificate-identity-rege ## Quick Deploy [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://marketplace.digitalocean.com/apps/bitwarden?action=deploy) -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/bitwarden.bitwarden-self-host) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/bitwardeninc.bitwarden-self-host) [Deploy to Kubernetes with Helm](https://github.com/bitwarden/helm-charts/blob/main/charts/self-host/README.md)