Bitwarden Lite Marketplace Images (#491)

* lite marketplace listings

* reuse ufw
This commit is contained in:
Kyle Spearrin
2026-04-15 13:26:22 -04:00
committed by GitHub
parent e469a532f5
commit 2dd1ef7f7a
15 changed files with 1851 additions and 2 deletions

4
.github/CODEOWNERS vendored
View File

@@ -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

182
.github/workflows/release-aws-lite.yml vendored Normal file
View File

@@ -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

180
.github/workflows/release-azure-lite.yml vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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 }
});

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 <<EOF
********************************************************************************
Welcome to your Bitwarden Lite server
https://bitwarden.com
Documentation:
https://bitwarden.com/help/install-and-deploy-lite/
Current configuration ($SETTINGS_FILE):
Domain: ${BW_DOMAIN}
Installation ID: ${BW_INSTALLATION_ID}
EOF
if [ "$NEEDS_CONFIG" -eq 1 ]; then
printf "Action required - the following settings still need to be configured:\n"
printf "${CONFIG_ITEMS}"
printf "\n Get your Installation ID and Key at: https://bitwarden.com/host/\n"
printf "\n After editing settings.env, restart Bitwarden Lite:\n"
printf " cd /home/bitwarden && docker compose up -d\n"
else
printf " Bitwarden Lite is configured. Access your vault at https://${BW_DOMAIN}\n"
fi
cat <<EOF
Common commands:
sudo -i -u bitwarden docker compose -f /home/bitwarden/docker-compose.yml stop
sudo -i -u bitwarden docker compose -f /home/bitwarden/docker-compose.yml restart
sudo -i -u bitwarden docker compose -f /home/bitwarden/docker-compose.yml pull && \\
sudo -i -u bitwarden docker compose -f /home/bitwarden/docker-compose.yml up -d
********************************************************************************
To delete this message of the day: rm -rf $(readlink -f ${0})
EOF

View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Scripts in this directory will be executed by cloud-init on the first boot of servers
# created from your image. Things like generating passwords, configuration requiring IP address
# or other items that will be unique to each instance should be done in scripts here.
#
# Setup Bitwarden Lite
# ref: https://bitwarden.com/help/install-and-deploy-lite/
#
# Generate a random database password
DB_PASSWORD=$(openssl rand -hex 16)
# Download docker-compose.yml
curl -L -s -o /home/bitwarden/docker-compose.yml \
"https://raw.githubusercontent.com/bitwarden/self-host/main/bitwarden-lite/docker-compose.yml"
# Download settings.env and set the generated database password
curl -L -s -o /home/bitwarden/settings.env \
"https://raw.githubusercontent.com/bitwarden/self-host/main/bitwarden-lite/settings.env"
sed -i "s|^BW_DB_PASSWORD=.*|BW_DB_PASSWORD=${DB_PASSWORD}|" /home/bitwarden/settings.env
# Update the MariaDB container password in docker-compose.yml to match
sed -i "s|MARIADB_PASSWORD: \"super_strong_password\"|MARIADB_PASSWORD: \"${DB_PASSWORD}\"|" \
/home/bitwarden/docker-compose.yml
chmod 600 /home/bitwarden/settings.env
chown bitwarden:bitwarden /home/bitwarden/docker-compose.yml /home/bitwarden/settings.env
# Start Bitwarden Lite with default configuration.
# Database migrations run automatically on first boot.
# Edit /home/bitwarden/settings.env to set your domain and installation keys,
# then restart with: cd /home/bitwarden && docker compose up -d
cd /home/bitwarden && docker compose up -d
#
# Setup Bitwarden Lite update cron
# ref: https://bitwarden.com/help/install-and-deploy-lite/
#
printf '#!/usr/bin/env bash\ncd /home/bitwarden && docker compose pull && docker compose up -d\n' \
> /etc/cron.weekly/bitwardenupdate
chmod +x /etc/cron.weekly/bitwardenupdate

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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)