diff --git a/README.md b/README.md index 7fbc117..6562ed7 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,42 @@ # flashbox :zap: :package: -flashbox is an opinionated Confidential VM (CVM) base image designed to run podman pod payloads. It provides a simple way to deploy containerized applications in a secure environment. +flashbox is an opinionated Confidential VM (CVM) base image built for podman pod payloads. With a focus on security balanced against TCB size, designed to give developers the simplest path to TDX VMs. + +## Why flashbox? + +One command, `./zap` - and you've got yourself a TDX box. + +⚠️ **IMPORTANT**: This is an early development release and is not production-ready software. Use with caution. ## Quick Start -1. Deploy the flashbox VM image - - [Bare Metal non-TDX](#bare-metal-non-tdx) - - [Bare Metal TDX](#bare-metal-tdx) - - [Azure Deployment](#azure-deployment) - - [GCP Deployment](#gcp-deployment) +1. Download the latest VM image from the releases page + +2. Deploy the flashbox VM: +```bash +# Local deployment (non-TDX) +./zap --mode normal --image flashbox.raw + +# Local deployment (TDX) +./zap --mode tdx --image flashbox.raw + +# Azure deployment +./zap azure myvm eastus flashbox.azure.vhd -2. Provision and start your containers: +# GCP deployment +./zap gcp myvm us-east4 flashbox.tar.gz +``` + +### Known Issues + +- Azure deployments may encounter an issue with the `--security-type` parameter. See [Azure CLI Issue #29207](https://github.com/Azure/azure-cli/issues/29207#issuecomment-2479343290) for the workaround. + +### Considerations +⚠️ **WARNING**: Debug releases come with SSH enabled and a root user without password. Always use the `--ssh-source-ip` option to restrict SSH access in cloud deployments. +⚠️ **IMPORTANT**: If you want to run TDX VMs on bare metal you need to first setup your host environment properly. For this, follow the instructions in the [canonical/tdx](https://github.com/canonical/tdx) repo. + +3. Provision and start your containers: ```bash # Upload pod configuration and environment variables curl -X POST -F "pod.yaml=@pod.yaml" -F "env=@env" http://flashbox:24070/upload @@ -22,16 +47,14 @@ curl -X POST http://flashbox:24070/start ## Pod Configuration -### Docker Compose Users - -If you're coming from Docker Compose, you can convert your existing configurations to podman pod format. The pod configuration format is similar to Kubernetes manifests (YAML manifests). +### Docker Compose Migration -To convert a Docker Compose file to a podman pod configuration: +If you're coming from Docker Compose, you can convert your existing configurations: ```bash podman-compose generate-k8s docker-compose.yml > pod.yaml ``` -See the [official documentation on differences between Docker Compose and Podman](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) for more details on migration. +See the [official documentation on differences between Docker Compose and Podman](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) for migration details. ### Example Configuration @@ -49,17 +72,12 @@ spec: - name: web-container image: nginx:latest env: - - name: DB_HOST - value: "localhost" - - name: API_PORT - value: "3000" - name: DATABASE_URL value: "${DATABASE_URL}" ports: - containerPort: 80 hostPort: 8080 ``` - ### Non-Attestable Variable Configuration flashbox allows you to provision secrets and configuration variables that should remain outside the attestation flow. This is done through a separate `env` file that is processed independently of the pod configuration. @@ -80,36 +98,3 @@ env: ``` Variables in the env file will be substituted into the pod configuration at runtime, keeping them separate from the attestation process. This is useful for both secrets and configuration that may vary between deployments. - -## Deployment Options - -### Bare Metal non-TDX -[TO BE FILLED: Bare metal non-TDX deployment instructions] - -### Bare Metal TDX -[TO BE FILLED: Bare metal TDX deployment instructions] - -### Azure Deployment -[TO BE FILLED: Azure deployment instructions] - -### GCP Deployment -[TO BE FILLED: GCP deployment instructions] - -## Security Considerations - -- flashbox runs in a Confidential VM environment, providing enhanced security for your workloads -- Configuration variables can be separated from pod configurations using the env file -- The env file contents are not included in the attestation flow, providing flexibility for deployment-specific configurations - -## API Endpoints - -- `POST http://flashbox:24070/upload`: Upload pod configuration and environment files -- `POST http://flashbox:24070/start`: Start the configured containers - -## Contributing - -[TO BE FILLED: Contributing guidelines] - -## License - -[TO BE FILLED: License information] diff --git a/lib/bm.sh b/lib/bm.sh new file mode 100644 index 0000000..244a57b --- /dev/null +++ b/lib/bm.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " --mode VM mode (default: normal)" + echo " --image PATH Path to VM image (required)" + echo " --ram SIZE RAM size in GB (default: 32)" + echo " --cpus NUMBER Number of CPUs (default: 16)" + echo " --ssh-port PORT SSH port forwarding (default: 10022)" + echo " --ports PORTS Additional ports to open, comma-separated" + echo " --name STRING Process name (default: qemu-vm)" + echo " --log PATH Log file path (default: /tmp/qemu-guest.log)" + echo " --ovmf PATH Path to OVMF firmware (default: /usr/share/ovmf/OVMF.fd)" + echo " --help Show this help message" + exit 1 +} + +cleanup() { + rm -f /tmp/qemu-guest*.log &> /dev/null + rm -f /tmp/qemu-*-monitor.sock &> /dev/null + + PID_QEMU=$(cat /tmp/qemu-pid.pid 2> /dev/null) + [ ! -z "$PID_QEMU" ] && echo "Cleanup, kill VM with PID: ${PID_QEMU}" && kill -TERM ${PID_QEMU} &> /dev/null + sleep 3 +} + +# Default values +MODE="normal" +RAM_SIZE="32" +CPUS="16" +SSH_PORT="10022" +ADDITIONAL_PORTS="" +PROCESS_NAME="qemu-vm" +LOGFILE="/tmp/qemu-guest.log" +OVMF_PATH="/usr/share/ovmf/OVMF.fd" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --mode) + MODE="$2" + shift 2 + ;; + --image) + VM_IMG="$2" + shift 2 + ;; + --ram) + RAM_SIZE="$2" + shift 2 + ;; + --cpus) + CPUS="$2" + shift 2 + ;; + --ssh-port) + SSH_PORT="$2" + shift 2 + ;; + --ports) + ADDITIONAL_PORTS="$2" + shift 2 + ;; + --name) + PROCESS_NAME="$2" + shift 2 + ;; + --log) + LOGFILE="$2" + shift 2 + ;; + --ovmf) + OVMF_PATH="$2" + shift 2 + ;; + --help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +# Check required parameters +if [ -z "$VM_IMG" ]; then + echo "Error: VM image path is required" + usage +fi + +# Verify mode +if [ "$MODE" != "normal" ] && [ "$MODE" != "tdx" ]; then + echo "Error: Invalid mode. Must be 'normal' or 'tdx'" + usage +fi + +# Check KVM group membership +if ! groups | grep -qw "kvm"; then + echo "Please add user $USER to kvm group to run this script (usermod -aG kvm $USER and then log in again)." + exit 1 +fi + +# Clean up any existing instances +cleanup +if [ "$1" = "clean" ]; then + exit 0 +fi + +# Prepare port forwarding string +PORT_FORWARDS="-device virtio-net-pci,netdev=nic0 -netdev user,id=nic0,hostfwd=tcp::${SSH_PORT}-:22" + +# Add default flashbox ports +PORT_FORWARDS="${PORT_FORWARDS},hostfwd=tcp::24070-:24070,hostfwd=tcp::24071-:24071" + +# Add additional ports if specified +if [ ! -z "$ADDITIONAL_PORTS" ]; then + IFS=',' read -ra PORTS <<< "$ADDITIONAL_PORTS" + for port in "${PORTS[@]}"; do + PORT_FORWARDS="${PORT_FORWARDS},hostfwd=tcp::${port}-:${port}" + done +fi + +# Base QEMU command +QEMU_CMD="qemu-system-x86_64 -D $LOGFILE \ + -accel kvm \ + -m ${RAM_SIZE}G -smp $CPUS \ + -name ${PROCESS_NAME},process=${PROCESS_NAME},debug-threads=on \ + -cpu host \ + -nographic \ + -nodefaults \ + ${PORT_FORWARDS} \ + -drive file=${VM_IMG},if=none,id=virtio-disk0 \ + -device virtio-blk-pci,drive=virtio-disk0 \ + -bios ${OVMF_PATH} \ + -chardev stdio,id=char0,mux=on,signal=off \ + -mon chardev=char0 \ + -serial chardev:char0 \ + -pidfile /tmp/qemu-pid.pid" + +# Add TDX-specific parameters if mode is tdx +if [ "$MODE" = "tdx" ]; then + QEMU_CMD="$QEMU_CMD \ + -object '{\"qom-type\":\"tdx-guest\",\"id\":\"tdx\",\"quote-generation-socket\":{\"type\": \"vsock\", \"cid\":\"2\",\"port\":\"4050\"}}' \ + -machine q35,kernel_irqchip=split,confidential-guest-support=tdx,hpet=off \ + -device vhost-vsock-pci,guest-cid=4" +else + QEMU_CMD="$QEMU_CMD \ + -machine q35" +fi + +# Execute QEMU command +eval $QEMU_CMD + +ret=$? +if [ $ret -ne 0 ]; then + echo "Error: Failed to create VM. Please check logfile \"$LOGFILE\" for more information." + exit $ret +fi + +PID_QEMU=$(cat /tmp/qemu-pid.pid) + +echo "VM started in $MODE mode with PID: ${PID_QEMU}" +echo "To login via SSH: ssh -p $SSH_PORT root@localhost" diff --git a/lib/cloud.sh b/lib/cloud.sh new file mode 100755 index 0000000..09f2a54 --- /dev/null +++ b/lib/cloud.sh @@ -0,0 +1,300 @@ +#!/bin/bash +set -e + +usage() { + echo "Usage: $0 [command] [options] [region] [image-path]" + echo "" + echo "Commands:" + echo " deploy Deploy a new VM (default if no command specified)" + echo " cleanup Remove all resources for the given name" + echo "" + echo "Cloud Platforms:" + echo " azure Deploy to Azure" + echo " gcp Deploy to Google Cloud Platform" + echo "" + echo "Arguments:" + echo " name Resource name/prefix for the deployment" + echo " region Cloud region to deploy in (default: eastus for Azure, us-east4 for GCP)" + echo " image-path Path to VM image (required for deploy)" + echo "" + echo "Options:" + echo " --machine-type TYPE VM size (default: Standard_EC4eds_v5 for Azure, c3-standard-4 for GCP)" + echo " --ports PORTS Additional ports to open, comma-separated (24070,24071 always open)" + echo " --ssh-source-ip IP Restrict SSH access to this IP address" + exit 1 +} + +check_dependencies() { + local cloud=$1 + if [[ "$cloud" == "azure" ]]; then + command -v az >/dev/null 2>&1 || { echo "Error: 'az' required"; exit 1; } + command -v azcopy >/dev/null 2>&1 || { echo "Error: 'azcopy' required"; exit 1; } + command -v jq >/dev/null 2>&1 || { echo "Error: 'jq' required"; exit 1; } + elif [[ "$cloud" == "gcp" ]]; then + command -v gcloud >/dev/null 2>&1 || { echo "Error: 'gcloud' required"; exit 1; } + fi +} + +cleanup_azure() { + local name=$1 + echo "Cleaning up Azure resources for $name..." + az group delete --name "$name" --yes +} + +cleanup_gcp() { + local name=$1 + local label="flashbox-deployment=$name" + + echo "Cleaning up GCP resources for $name..." + + # Delete VM instance + gcloud compute instances delete "$name" --quiet || true + + # Delete image + gcloud compute images delete "$name" --quiet || true + + # Delete firewall rules + gcloud compute firewall-rules list --filter="labels.$label" --format="get(name)" | \ + while read -r rule; do + gcloud compute firewall-rules delete "$rule" --quiet || true + done + + # Delete network + gcloud compute networks delete "$name" --quiet || true + + # Delete storage bucket + gcloud storage rm -r "gs://${name}" || true +} + +create_azure_deployment() { + local name=$1 + local region=$2 + local image_path=$3 + local machine_type=${4:-"Standard_EC4eds_v5"} + local ssh_source_ip=$5 + local additional_ports=$6 + + # Create resource group + echo "Creating resource group..." + az group create --name "$name" --location "$region" + + # Create and upload disk + echo "Creating and uploading disk..." + local disk_size=$(wc -c < "$image_path") + az disk create -n "$name" -g "$name" -l "$region" \ + --os-type Linux \ + --upload-type Upload \ + --upload-size-bytes "$disk_size" \ + --sku standard_lrs \ + --security-type ConfidentialVM_NonPersistedTPM \ + --hyper-v-generation V2 + + # Upload VHD + local sas_json=$(az disk grant-access -n "$name" -g "$name" --access-level Write --duration-in-seconds 86400) + local sas_uri=$(echo "$sas_json" | jq -r '.accessSas') + azcopy copy "$image_path" "$sas_uri" --blob-type PageBlob + az disk revoke-access -n "$name" -g "$name" + + # Create NSG with base rules + echo "Creating network security group..." + az network nsg create --name "$name" --resource-group "$name" --location "$region" + + # Add a small delay to ensure NSG is fully created + sleep 5 + + # Add SSH rule with optional IP restriction + local ssh_source="${ssh_source_ip:-*}" + az network nsg rule create --nsg-name "$name" --resource-group "$name" \ + --name AllowSSH --priority 100 \ + --source-address-prefixes "$ssh_source" \ + --destination-port-ranges 22 --access Allow --protocol Tcp + + # Add default ports + az network nsg rule create --nsg-name "$name" --resource-group "$name" \ + --name "FlashboxAPI" --priority 200 \ + --destination-port-ranges 24070-24071 --access Allow --protocol Tcp + + # Add additional port rules if specified + if [[ -n "$additional_ports" ]]; then + IFS=',' read -ra PORTS <<< "$additional_ports" + local priority=300 + for port in "${PORTS[@]}"; do + az network nsg rule create --nsg-name "$name" --resource-group "$name" \ + --name "Port_${port}" --priority $priority \ + --destination-port-ranges "$port" --access Allow --protocol Tcp + ((priority+=1)) + done + fi + + # Create VM + echo "Creating VM..." + az vm create --name "$name" \ + --resource-group "$name" \ + --size "$machine_type" \ + --attach-os-disk "$name" \ + --security-type ConfidentialVM \ + --enable-vtpm true \ + --enable-secure-boot false \ + --os-disk-security-encryption-type NonPersistedTPM \ + --os-type Linux \ + --nsg "$name" +} + +create_gcp_deployment() { + local name=$1 + local region=$2 + local image_path=$3 + local machine_type=${4:-"c3-standard-4"} + local ssh_source_ip=$5 + local additional_ports=$6 + + local zone="${region}-b" # Assuming zone b + local deployment_label="flashbox-deployment=$name" + + # Create network if it doesn't exist + echo "Creating network..." + gcloud compute networks create "$name" \ + --subnet-mode=auto \ + --labels="$deployment_label" || true + + # Create firewall rules + echo "Creating firewall rules..." + + # SSH rule with optional IP restriction + local ssh_source="${ssh_source_ip:-0.0.0.0/0}" + gcloud compute firewall-rules create "${name}-ssh" \ + --network="$name" \ + --allow=tcp:22 \ + --source-ranges="$ssh_source" \ + --labels="$deployment_label" + + # Default ports + gcloud compute firewall-rules create "${name}-flashbox" \ + --network="$name" \ + --allow=tcp:24070-24071 \ + --labels="$deployment_label" + + # Additional ports if specified + if [[ -n "$additional_ports" ]]; then + local ports_list="tcp:${additional_ports//,/,tcp:}" + gcloud compute firewall-rules create "${name}-ports" \ + --network="$name" \ + --allow="$ports_list" \ + --labels="$deployment_label" + fi + + # Upload and create image + echo "Creating storage bucket and uploading image..." + gcloud storage buckets create "gs://${name}" --labels="$deployment_label" + gcloud storage cp "$image_path" "gs://${name}/image.tar.gz" + + echo "Creating VM image..." + gcloud compute images create "$name" \ + --source-uri="gs://${name}/image.tar.gz" \ + --guest-os-features=UEFI_COMPATIBLE,VIRTIO_SCSI_MULTIQUEUE,GVNIC,TDX_CAPABLE \ + --labels="$deployment_label" + + echo "Creating VM..." + gcloud compute instances create "$name" \ + --zone="$zone" \ + --machine-type="$machine_type" \ + --network="$name" \ + --image="$name" \ + --confidential-compute-type=TDX \ + --maintenance-policy=TERMINATE \ + --labels="$deployment_label" +} + +# Parse command line arguments +COMMAND="deploy" +CLOUD="" +NAME="" +REGION="" +IMAGE_PATH="" +MACHINE_TYPE="" +SSH_SOURCE_IP="" +ADDITIONAL_PORTS="" + +# Check if first arg is a command +case $1 in + deploy|cleanup) + COMMAND="$1" + shift + ;; +esac + +while [[ $# -gt 0 ]]; do + case $1 in + --machine-type) + MACHINE_TYPE="$2" + shift 2 + ;; + --ports) + ADDITIONAL_PORTS="$2" + shift 2 + ;; + --ssh-source-ip) + SSH_SOURCE_IP="$2" + shift 2 + ;; + --help) + usage + ;; + *) + if [[ -z "$CLOUD" ]]; then + CLOUD="$1" + elif [[ -z "$NAME" ]]; then + NAME="$1" + elif [[ -z "$REGION" ]]; then + REGION="$1" + elif [[ -z "$IMAGE_PATH" ]]; then + IMAGE_PATH="$1" + else + echo "Unknown argument: $1" + usage + fi + shift + ;; + esac +done + +# Validate required arguments +if [[ -z "$CLOUD" ]] || [[ -z "$NAME" ]]; then + echo "Error: Missing required arguments" + usage +fi + +if [[ "$CLOUD" != "azure" ]] && [[ "$CLOUD" != "gcp" ]]; then + echo "Error: Invalid cloud platform. Must be 'azure' or 'gcp'" + usage +fi + +# Set default region if not specified for deploy command +if [[ "$COMMAND" == "deploy" && -z "$REGION" ]]; then + REGION=$([ "$CLOUD" == "azure" ] && echo "westeurope" || echo "us-east4") +fi + +# Execute command +case $COMMAND in + deploy) + if [[ -z "$IMAGE_PATH" ]]; then + echo "Error: Image path required for deploy command" + usage + fi + check_dependencies "$CLOUD" + if [[ "$CLOUD" == "azure" ]]; then + create_azure_deployment "$NAME" "$REGION" "$IMAGE_PATH" "$MACHINE_TYPE" "$SSH_SOURCE_IP" "$ADDITIONAL_PORTS" + else + create_gcp_deployment "$NAME" "$REGION" "$IMAGE_PATH" "$MACHINE_TYPE" "$SSH_SOURCE_IP" "$ADDITIONAL_PORTS" + fi + ;; + cleanup) + if [[ "$CLOUD" == "azure" ]]; then + cleanup_azure "$NAME" + else + cleanup_gcp "$NAME" + fi + ;; +esac + +echo "${COMMAND} complete!" diff --git a/zap b/zap new file mode 100755 index 0000000..e15e943 --- /dev/null +++ b/zap @@ -0,0 +1,15 @@ +#!/bin/bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Early detection if this is a cloud operation +is_cloud_operation() { + [[ "$1" == "azure" ]] || [[ "$1" == "gcp" ]] || \ + [[ "$2" == "azure" ]] || [[ "$2" == "gcp" ]] +} + +if is_cloud_operation "$1" "$2"; then + exec "${SCRIPT_DIR}/lib/cloud.sh" "$@" +else + exec "${SCRIPT_DIR}/lib/bm.sh" "$@" +fi