diff --git a/.circleci/config.yml b/.circleci/config.yml index dd5eecb..4ee5f6a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,12 +27,11 @@ jobs: echo "${RES}" exit 1 fi - - run: - name: Install golangci-lint - command: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.6.0 - run: name: GolangCI Lint - command: golangci-lint run --timeout 300s + command: | + golangci-lint --version + golangci-lint run --verbose - save_cache: &save-cache paths: - /home/circleci/go/pkg/mod diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4dee6ef..5ff90f8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,7 +5,7 @@ Always reference these instructions first and fallback to search or bash command ## Working Effectively ### Bootstrap and Dependencies -- Install Go 1.25+: `go version` must show go1.25 or later +- Install Go 1.25.1+: `go version` must show go1.25.1 or later - Install Docker: Required for Vault development server - Install CLI tools for testing: ```bash @@ -13,7 +13,7 @@ Always reference these instructions first and fallback to search or bash command sudo apt-get update && sudo apt-get install -y curl jq # Check installations - go version # Must be 1.25+ + go version # Must be 1.25.1+ docker --version curl --version jq --version @@ -22,7 +22,7 @@ Always reference these instructions first and fallback to search or bash command ### Download Dependencies and Build - Download Go modules: `go mod download` -- takes 1-2 minutes. NEVER CANCEL. Set timeout to 180+ seconds. - Build binary: `go build -o sup3rs3cret cmd/sup3rS3cretMes5age/main.go` -- takes <1 second after dependencies downloaded. -- Install linter: `curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.8` -- takes 30-60 seconds. +- Install linter: `curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2` -- takes 30-60 seconds. Current system has v2.7.2. ### Testing and Validation - Run tests: `make test` -- takes 2-3 minutes. NEVER CANCEL. Set timeout to 300+ seconds. @@ -51,17 +51,39 @@ The application will start on port 8080. Access at http://localhost:8080 docker stop vault-dev && docker rm vault-dev ``` -### Docker Build Issues -**IMPORTANT**: Docker builds currently fail in CI/containerized environments due to certificate verification issues with Go proxy: -``` -go: cloud.google.com/go@v0.112.1: Get "https://proxy.golang.org/...": tls: failed to verify certificate: x509: certificate signed by unknown authority +### Docker Build and Deployment +The project includes comprehensive Docker support: + +#### Local Development with Docker Compose +```bash +# Start full stack (Vault + App on port 8082) +make run +# or +docker compose -f deploy/docker-compose.yml up --build -d + +# View logs +make logs + +# Stop services +make stop + +# Clean up +make clean ``` -Do NOT attempt Docker builds (`make build`, `make image`, `docker compose up --build`) in sandboxed environments. These commands will fail after 15-30 seconds. Use local Go builds instead. +The default `docker-compose.yml` runs the app on port 8082 (HTTP) with Vault using token `supersecret`. + +#### Production Docker Image +```bash +# Build multi-platform image with attestations +make image +# Builds for linux/amd64 and linux/arm64 with SBOM and provenance -If you need to test Docker functionality, run individual commands: -- `make build` -- WILL FAIL in CI. Takes 15-30 seconds to fail. -- `make image` -- WILL FAIL in CI. Takes 15-30 seconds to fail. +# Alternative: Build local image only +docker compose -f deploy/docker-compose.yml build +``` + +**Note**: In some CI/containerized environments, Docker builds may encounter certificate verification issues with Go proxy. If this occurs, use local Go builds instead. ## Validation @@ -100,6 +122,16 @@ Always run these commands before committing: ## Common Tasks +### Key Application Features +- **Self-Destructing Messages**: Messages are automatically deleted after first read +- **Vault Backend**: Uses HashiCorp Vault's cubbyhole for secure temporary storage +- **TTL Support**: Configurable time-to-live (default 48h, max 168h/7 days) +- **File Upload**: Support for file uploads with base64 encoding (max 50MB) +- **One-Time Tokens**: Vault tokens with exactly 2 uses (1 to create, 1 to read) +- **Rate Limiting**: 10 requests per second to prevent abuse +- **TLS Support**: Auto TLS via Let's Encrypt or manual certificate configuration +- **No External Dependencies**: All JavaScript/fonts self-hosted for privacy + ### Configuration Environment Variables - `VAULT_ADDR`: Vault server address (e.g., `http://localhost:8200`) - `VAULT_TOKEN`: Vault authentication token (e.g., `supersecret` for dev) @@ -114,22 +146,45 @@ Always run these commands before committing: ### Repository Structure ``` . -├── cmd/sup3rS3cretMes5age/main.go # Application entry point +├── cmd/sup3rS3cretMes5age/ +│ └── main.go # Application entry point (23 lines) ├── internal/ # Core application logic -│ ├── config.go # Configuration handling -│ ├── handlers.go # HTTP request handlers -│ ├── server.go # Web server setup -│ └── vault.go # Vault integration +│ ├── config.go # Configuration handling (77 lines) +│ ├── handlers.go # HTTP request handlers (88 lines) +│ ├── handlers_test.go # Handler unit tests (87 lines) +│ ├── server.go # Web server setup (94 lines) +│ ├── vault.go # Vault integration (174 lines) +│ └── vault_test.go # Vault unit tests (66 lines) ├── web/static/ # Frontend assets (HTML, CSS, JS) +│ ├── index.html # Main page (5KB) +│ ├── getmsg.html # Message retrieval page (7.8KB) +│ ├── application.css # Styling (2.3KB) +│ ├── clipboard-2.0.11.min.js # Copy functionality (9KB) +│ ├── montserrat.css # Font definitions +│ ├── robots.txt # Search engine rules +│ ├── fonts/ # Self-hosted Montserrat font files +│ └── icons/ # Favicon and app icons ├── deploy/ # Docker and deployment configs -│ ├── Dockerfile # Container build (fails in CI) -│ ├── docker-compose.yml # Local development stack -│ └── charts/ # Helm charts for Kubernetes -├── Makefile # Build automation -├── go.mod # Go module definition -└── README.md # Project documentation -``` - +│ ├── Dockerfile # Multi-stage container build +│ ├── docker-compose.yml # Local development stack (Vault + App) +│ └── charts/supersecretmessage/ # Helm c(lint + test pipeline) +.codacy.yml # Code quality config +.dockerignore # Docker ignore patterns +.git/ # Git repository data +.github/ # GitHub configuration (copilot-instructions.md) +.gitignore # Git ignore patterns +CLI.md # Command-line usage guide (313 lines, Bash/Zsh/Fish examples) +CODEOWNERS # GitHub code owners +LICENSE # MIT license +Makefile # Build targets (test, image, build, run, logs, stop, clean) +Makefile.buildx # Advanced buildx targets (multi-platform, AWS ECR) +README.md # Main documentation (176 lines) +cmd/ # Application entry points +deploy/ # Deployment configurations (Docker, Helm) +go.mod # Go module file (go 1.25.1) +go.sum # Go dependency checksums +internal/ # Internal packages (609 lines total) +web/ # Web assets (static HTML, CSS, JS, fonts, icons) ### Frequently Used Commands Output #### Repository Root Files @@ -157,14 +212,14 @@ web/ # Web assets ```go module github.com/algolia/sup3rS3cretMes5age -go 1.25 +go 1.25.1 require ( - github.com/hashicorp/vault v1.16.3 - github.com/hashicorp/vault/api v1.14.0 + github.com/hashicorp/vault v1.21.0 + github.com/hashicorp/vault/api v1.22.0 github.com/labstack/echo/v4 v4.13.4 - github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.40.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.45.0 ) ``` @@ -195,8 +250,8 @@ o() { ### Troubleshooting **"go: ... tls: failed to verify certificate"** -- This occurs in Docker builds in CI environments -- Use local Go builds instead: `go build cmd/sup3rS3cretMes5age/main.go` +- This may occur in Docker builds in some CI environments +- Solution: Use local Go builds instead: `go build -o sup3rs3cret cmd/sup3rS3cretMes5age/main.go` **"jq: command not found"** ```bash @@ -216,3 +271,48 @@ brew install jq - Tests create their own Vault instances - Verbose logging is normal (200+ lines per test) - NEVER CANCEL tests - they clean up automatically + +**Port 8082 already in use** +```bash +# Find what's using the port +sudo lsof -i :8082 +# or +sudo netstat -tulpn | grep 8082 + +# Stop docker-compose if running +make stop +``` + +**Build fails with "cannot find package"** +```bash +# Clean Go module cache and re-download +go clean -modcache +go mod download +``` + +### Makefile Targets Reference +```bash +make test # Run all unit tests (takes 2-3 min) +make image # Build multi-platform Docker image with attestations +make build # Build Docker image via docker-compose +make run # Start docker-compose stack (Vault + App on :8082) +make run-local # Clean and start docker-compose +make logs # Tail docker-compose logs +make stop # Stop docker-compose services +make clean # Remove docker-compose containers +``` + +### CircleCI Pipeline +The project uses CircleCI with two jobs: +1. **lint**: Format checking (gofmt), golangci-lint v2.6.0 +2. **test**: Unit tests via `make test` + +Pipeline runs on Go 1.25 docker image (`cimg/go:1.25`). + +### Helm Deployment +Helm chart located in `deploy/charts/supersecretmessage/`: +- Chart version: 0.1.0 +- App version: 0.2.5 +- Includes: Deployment, Service, Ingress, HPA, ServiceAccount +- Configurable: Vault connection, TLS settings, resource limits +- See [deploy/charts/README.md](deploy/charts/README.md) for details diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..da0a3a5 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,6 @@ +# Config format version +version: 2 + +run: + concurrency: 1 + timeout: 8m diff --git a/AWS_DEPLOYMENT.md b/AWS_DEPLOYMENT.md new file mode 100644 index 0000000..c2fd07b --- /dev/null +++ b/AWS_DEPLOYMENT.md @@ -0,0 +1,705 @@ +# AWS Deployment Guide + +This guide provides step-by-step instructions for deploying sup3rS3cretMes5age on AWS using various services. The application consists of two main components: +- The sup3rS3cretMes5age web application +- A HashiCorp Vault server for secure secret storage + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Option 1: ECS with Fargate (Recommended)](#option-1-ecs-with-fargate-recommended) +3. [Option 2: EKS (Kubernetes)](#option-2-eks-kubernetes) +4. [Option 3: EC2 with Docker](#option-3-ec2-with-docker) +5. [Security Considerations](#security-considerations) +6. [Cost Optimization](#cost-optimization) +7. [Troubleshooting](#troubleshooting) + +## Prerequisites + +Before starting, ensure you have: + +1. **AWS CLI** installed and configured with appropriate permissions +2. **Docker** installed for building and testing images locally +3. **Domain name** (recommended for HTTPS with Let's Encrypt) +4. **AWS Account** with the following IAM permissions: + - ECS full access + - ECR full access + - Application Load Balancer management + - VPC management + - IAM role creation + - Route 53 (if using custom domain) + +## Option 1: ECS with Fargate (Recommended) + +This option uses AWS ECS with Fargate for a serverless container deployment, which is cost-effective and easy to manage. + +### Step 1: Set up ECR Repository + +First, create a private ECR repository to store your Docker images: + +```bash +# Set your AWS region +export AWS_REGION=us-east-1 +export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + +# Create ECR repositories +aws ecr create-repository \ + --repository-name sup3rs3cretmes5age \ + --region $AWS_REGION + +aws ecr create-repository \ + --repository-name vault \ + --region $AWS_REGION + +# Get login token for ECR +aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com +``` + +### Step 2: Build and Push Docker Images + +Build and push the application image to ECR: + +```bash +# Clone and build the application +git clone https://github.com/algolia/sup3rS3cretMes5age.git +cd sup3rS3cretMes5age + +# Build the application image (with network=host to handle certificate issues) +docker build --network=host -f deploy/Dockerfile -t sup3rs3cretmes5age . + +# Tag for ECR +docker tag sup3rs3cretmes5age:latest $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/sup3rs3cretmes5age:latest + +# Push to ECR +docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/sup3rs3cretmes5age:latest + +# Pull and push Vault image to your ECR +docker pull hashicorp/vault:latest +docker tag hashicorp/vault:latest $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/vault:latest +docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/vault:latest +``` + +### Step 3: Create VPC and Security Groups + +Create a VPC with public and private subnets: + +```bash +# Create VPC +export VPC_ID=$(aws ec2 create-vpc \ + --cidr-block 10.0.0.0/16 \ + --query 'Vpc.VpcId' \ + --output text) + +aws ec2 create-tags \ + --resources $VPC_ID \ + --tags Key=Name,Value=sup3rs3cretmes5age-vpc + +# Create Internet Gateway +export IGW_ID=$(aws ec2 create-internet-gateway \ + --query 'InternetGateway.InternetGatewayId' \ + --output text) + +aws ec2 attach-internet-gateway \ + --vpc-id $VPC_ID \ + --internet-gateway-id $IGW_ID + +# Create public subnets in two AZs +export PUBLIC_SUBNET_1=$(aws ec2 create-subnet \ + --vpc-id $VPC_ID \ + --cidr-block 10.0.1.0/24 \ + --availability-zone ${AWS_REGION}a \ + --query 'Subnet.SubnetId' \ + --output text) + +export PUBLIC_SUBNET_2=$(aws ec2 create-subnet \ + --vpc-id $VPC_ID \ + --cidr-block 10.0.2.0/24 \ + --availability-zone ${AWS_REGION}b \ + --query 'Subnet.SubnetId' \ + --output text) + +# Create private subnets +export PRIVATE_SUBNET_1=$(aws ec2 create-subnet \ + --vpc-id $VPC_ID \ + --cidr-block 10.0.3.0/24 \ + --availability-zone ${AWS_REGION}a \ + --query 'Subnet.SubnetId' \ + --output text) + +export PRIVATE_SUBNET_2=$(aws ec2 create-subnet \ + --vpc-id $VPC_ID \ + --cidr-block 10.0.4.0/24 \ + --availability-zone ${AWS_REGION}b \ + --query 'Subnet.SubnetId' \ + --output text) + +# Create route table for public subnets +export PUBLIC_RT=$(aws ec2 create-route-table \ + --vpc-id $VPC_ID \ + --query 'RouteTable.RouteTableId' \ + --output text) + +aws ec2 create-route \ + --route-table-id $PUBLIC_RT \ + --destination-cidr-block 0.0.0.0/0 \ + --gateway-id $IGW_ID + +# Associate public subnets with route table +aws ec2 associate-route-table --subnet-id $PUBLIC_SUBNET_1 --route-table-id $PUBLIC_RT +aws ec2 associate-route-table --subnet-id $PUBLIC_SUBNET_2 --route-table-id $PUBLIC_RT + +# Enable auto-assign public IPs for public subnets +aws ec2 modify-subnet-attribute --subnet-id $PUBLIC_SUBNET_1 --map-public-ip-on-launch +aws ec2 modify-subnet-attribute --subnet-id $PUBLIC_SUBNET_2 --map-public-ip-on-launch +``` + +Create security groups: + +```bash +# Security group for Application Load Balancer +export ALB_SG=$(aws ec2 create-security-group \ + --group-name sup3rs3cretmes5age-alb-sg \ + --description "Security group for sup3rs3cretmes5age ALB" \ + --vpc-id $VPC_ID \ + --query 'GroupId' \ + --output text) + +# Allow HTTP and HTTPS traffic to ALB +aws ec2 authorize-security-group-ingress \ + --group-id $ALB_SG \ + --protocol tcp \ + --port 80 \ + --cidr 0.0.0.0/0 + +aws ec2 authorize-security-group-ingress \ + --group-id $ALB_SG \ + --protocol tcp \ + --port 443 \ + --cidr 0.0.0.0/0 + +# Security group for ECS tasks +export ECS_SG=$(aws ec2 create-security-group \ + --group-name sup3rs3cretmes5age-ecs-sg \ + --description "Security group for sup3rs3cretmes5age ECS tasks" \ + --vpc-id $VPC_ID \ + --query 'GroupId' \ + --output text) + +# Allow traffic from ALB to ECS tasks +aws ec2 authorize-security-group-ingress \ + --group-id $ECS_SG \ + --protocol tcp \ + --port 80 \ + --source-group $ALB_SG + +# Allow Vault communication between tasks +aws ec2 authorize-security-group-ingress \ + --group-id $ECS_SG \ + --protocol tcp \ + --port 8200 \ + --source-group $ECS_SG +``` + +### Step 4: Create Application Load Balancer + +```bash +# Create Application Load Balancer +export ALB_ARN=$(aws elbv2 create-load-balancer \ + --name sup3rs3cretmes5age-alb \ + --subnets $PUBLIC_SUBNET_1 $PUBLIC_SUBNET_2 \ + --security-groups $ALB_SG \ + --query 'LoadBalancers[0].LoadBalancerArn' \ + --output text) + +# Get ALB DNS name +export ALB_DNS=$(aws elbv2 describe-load-balancers \ + --load-balancer-arns $ALB_ARN \ + --query 'LoadBalancers[0].DNSName' \ + --output text) + +echo "ALB DNS Name: $ALB_DNS" + +# Create target group +export TARGET_GROUP_ARN=$(aws elbv2 create-target-group \ + --name sup3rs3cretmes5age-tg \ + --protocol HTTP \ + --port 80 \ + --vpc-id $VPC_ID \ + --target-type ip \ + --health-check-path / \ + --health-check-interval-seconds 30 \ + --health-check-timeout-seconds 5 \ + --healthy-threshold-count 2 \ + --unhealthy-threshold-count 3 \ + --query 'TargetGroups[0].TargetGroupArn' \ + --output text) + +# Create listener +aws elbv2 create-listener \ + --load-balancer-arn $ALB_ARN \ + --protocol HTTP \ + --port 80 \ + --default-actions Type=forward,TargetGroupArn=$TARGET_GROUP_ARN +``` + +### Step 5: Set up ECS Cluster and Task Definitions + +Create ECS cluster: + +```bash +# Create ECS cluster +aws ecs create-cluster --cluster-name sup3rs3cretmes5age-cluster +``` + +Create IAM roles for ECS: + +```bash +# Create ECS task execution role +cat > ecs-task-execution-role-trust-policy.json << EOF +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +EOF + +aws iam create-role \ + --role-name ecsTaskExecutionRole \ + --assume-role-policy-document file://ecs-task-execution-role-trust-policy.json + +aws iam attach-role-policy \ + --role-name ecsTaskExecutionRole \ + --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + +# Get the role ARN +export TASK_EXECUTION_ROLE_ARN=$(aws iam get-role \ + --role-name ecsTaskExecutionRole \ + --query 'Role.Arn' \ + --output text) +``` + +Create task definition: + +```bash +cat > task-definition.json << EOF +{ + "family": "sup3rs3cretmes5age", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "512", + "memory": "1024", + "executionRoleArn": "$TASK_EXECUTION_ROLE_ARN", + "containerDefinitions": [ + { + "name": "vault", + "image": "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/vault:latest", + "essential": true, + "environment": [ + { + "name": "VAULT_DEV_ROOT_TOKEN_ID", + "value": "supersecret" + } + ], + "portMappings": [ + { + "containerPort": 8200, + "protocol": "tcp" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/sup3rs3cretmes5age", + "awslogs-region": "$AWS_REGION", + "awslogs-stream-prefix": "vault" + } + }, + "linuxParameters": { + "capabilities": { + "add": ["IPC_LOCK"] + } + } + }, + { + "name": "supersecret", + "image": "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/sup3rs3cretmes5age:latest", + "essential": true, + "dependsOn": [ + { + "containerName": "vault", + "condition": "START" + } + ], + "environment": [ + { + "name": "VAULT_ADDR", + "value": "http://localhost:8200" + }, + { + "name": "VAULT_TOKEN", + "value": "supersecret" + }, + { + "name": "SUPERSECRETMESSAGE_HTTP_BINDING_ADDRESS", + "value": ":80" + } + ], + "portMappings": [ + { + "containerPort": 80, + "protocol": "tcp" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/sup3rs3cretmes5age", + "awslogs-region": "$AWS_REGION", + "awslogs-stream-prefix": "supersecret" + } + } + } + ] +} +EOF + +# Create CloudWatch log group +aws logs create-log-group --log-group-name /ecs/sup3rs3cretmes5age + +# Register task definition +aws ecs register-task-definition --cli-input-json file://task-definition.json +``` + +### Step 6: Create ECS Service + +```bash +# Create ECS service +cat > service-definition.json << EOF +{ + "serviceName": "sup3rs3cretmes5age-service", + "cluster": "sup3rs3cretmes5age-cluster", + "taskDefinition": "sup3rs3cretmes5age", + "desiredCount": 1, + "launchType": "FARGATE", + "networkConfiguration": { + "awsvpcConfiguration": { + "subnets": ["$PRIVATE_SUBNET_1", "$PRIVATE_SUBNET_2"], + "securityGroups": ["$ECS_SG"], + "assignPublicIp": "DISABLED" + } + }, + "loadBalancers": [ + { + "targetGroupArn": "$TARGET_GROUP_ARN", + "containerName": "supersecret", + "containerPort": 80 + } + ] +} +EOF + +aws ecs create-service --cli-input-json file://service-definition.json +``` + +### Step 7: Configure Domain and HTTPS (Optional but Recommended) + +If you have a domain name, you can configure HTTPS: + +```bash +# Request SSL certificate (replace with your domain) +export DOMAIN_NAME="secrets.yourdomain.com" + +export CERT_ARN=$(aws acm request-certificate \ + --domain-name $DOMAIN_NAME \ + --validation-method DNS \ + --query 'CertificateArn' \ + --output text) + +echo "Certificate ARN: $CERT_ARN" +echo "Complete DNS validation in ACM console, then continue..." + +# After DNS validation is complete, create HTTPS listener +aws elbv2 create-listener \ + --load-balancer-arn $ALB_ARN \ + --protocol HTTPS \ + --port 443 \ + --certificates CertificateArn=$CERT_ARN \ + --default-actions Type=forward,TargetGroupArn=$TARGET_GROUP_ARN + +# Create Route 53 record (if using Route 53) +# You'll need to create this manually or use your DNS provider +``` + +## Option 2: EKS (Kubernetes) + +For teams already using Kubernetes, you can deploy using the provided Helm chart on Amazon EKS. + +### Prerequisites for EKS Deployment + +```bash +# Install required tools +# - kubectl +# - helm +# - eksctl (recommended for cluster creation) + +# Create EKS cluster +eksctl create cluster \ + --name sup3rs3cretmes5age \ + --region $AWS_REGION \ + --nodegroup-name standard-workers \ + --node-type t3.medium \ + --nodes 2 \ + --nodes-min 1 \ + --nodes-max 4 \ + --managed +``` + +### Deploy with Helm + +```bash +# Add Vault Helm repository +helm repo add hashicorp https://helm.releases.hashicorp.com +helm repo update + +# Install Vault +helm install vault hashicorp/vault \ + --set "server.dev.enabled=true" \ + --set "server.dev.devRootToken=supersecret" + +# Deploy sup3rS3cretMes5age using the provided Helm chart +cd sup3rS3cretMes5age/deploy/charts/supersecretmessage + +# Update values.yaml for your configuration +helm install sup3rs3cretmes5age . \ + --set image.repository=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/sup3rs3cretmes5age \ + --set image.tag=latest \ + --set vault.address=http://vault:8200 \ + --set vault.token=supersecret + +# Create ingress for external access +kubectl apply -f - << EOF +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: sup3rs3cretmes5age-ingress + annotations: + kubernetes.io/ingress.class: alb + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/target-type: ip +spec: + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sup3rs3cretmes5age + port: + number: 80 +EOF +``` + +## Option 3: EC2 with Docker + +For a simpler setup, you can deploy on EC2 instances using Docker Compose. + +### Step 1: Launch EC2 Instance + +```bash +# Create key pair +aws ec2 create-key-pair \ + --key-name sup3rs3cretmes5age-key \ + --query 'KeyMaterial' \ + --output text > sup3rs3cretmes5age-key.pem + +chmod 400 sup3rs3cretmes5age-key.pem + +# Launch EC2 instance +export INSTANCE_ID=$(aws ec2 run-instances \ + --image-id ami-0c55b159cbfafe1d0 \ + --count 1 \ + --instance-type t3.small \ + --key-name sup3rs3cretmes5age-key \ + --security-groups default \ + --query 'Instances[0].InstanceId' \ + --output text) + +# Get public IP +export INSTANCE_IP=$(aws ec2 describe-instances \ + --instance-ids $INSTANCE_ID \ + --query 'Reservations[0].Instances[0].PublicIpAddress' \ + --output text) +``` + +### Step 2: Configure EC2 Instance + +```bash +# SSH to instance and set up Docker +ssh -i sup3rs3cretmes5age-key.pem ec2-user@$INSTANCE_IP + +# On the EC2 instance: +sudo yum update -y +sudo yum install -y docker +sudo service docker start +sudo usermod -a -G docker ec2-user + +# Install Docker Compose +sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose + +# Clone repository +git clone https://github.com/algolia/sup3rS3cretMes5age.git +cd sup3rS3cretMes5age + +# Start services +docker-compose -f deploy/docker-compose.yml up -d +``` + +## Security Considerations + +### 1. Use AWS Secrets Manager for Vault Token + +Instead of hardcoding the Vault token, use AWS Secrets Manager: + +```bash +# Create secret +aws secretsmanager create-secret \ + --name sup3rs3cretmes5age/vault-token \ + --description "Vault root token for sup3rs3cretmes5age" \ + --secret-string "your-secure-vault-token" + +# Update task definition to use secrets +# Add to containerDefinitions[].secrets: +{ + "name": "VAULT_TOKEN", + "valueFrom": "arn:aws:secretsmanager:region:account:secret:sup3rs3cretmes5age/vault-token" +} +``` + +### 2. Enable VPC Flow Logs + +```bash +aws ec2 create-flow-logs \ + --resource-type VPC \ + --resource-ids $VPC_ID \ + --traffic-type ALL \ + --log-destination-type cloud-watch-logs \ + --log-group-name VPCFlowLogs +``` + +### 3. Use HTTPS Only + +Always configure HTTPS and redirect HTTP traffic: + +```bash +# Modify task definition to enable HTTPS redirect +{ + "name": "SUPERSECRETMESSAGE_HTTPS_REDIRECT_ENABLED", + "value": "true" +} +``` + +### 4. Implement Network ACLs + +Create restrictive network ACLs for additional security: + +```bash +# Create network ACL +export NACL_ID=$(aws ec2 create-network-acl \ + --vpc-id $VPC_ID \ + --query 'NetworkAcl.NetworkAclId' \ + --output text) + +# Add rules as needed for your security requirements +``` + +## Cost Optimization + +### 1. Use Fargate Spot for Development + +For non-production environments, consider using Fargate Spot: + +```bash +# Update service to use Fargate Spot +aws ecs put-cluster-capacity-providers \ + --cluster sup3rs3cretmes5age-cluster \ + --capacity-providers FARGATE FARGATE_SPOT +``` + +### 2. Auto Scaling + +Configure auto scaling for production workloads: + +```bash +# Register scalable target +aws application-autoscaling register-scalable-target \ + --service-namespace ecs \ + --scalable-dimension ecs:service:DesiredCount \ + --resource-id service/sup3rs3cretmes5age-cluster/sup3rs3cretmes5age-service \ + --min-capacity 1 \ + --max-capacity 10 + +# Create scaling policy +aws application-autoscaling put-scaling-policy \ + --policy-name sup3rs3cretmes5age-scaling-policy \ + --service-namespace ecs \ + --scalable-dimension ecs:service:DesiredCount \ + --resource-id service/sup3rs3cretmes5age-cluster/sup3rs3cretmes5age-service \ + --policy-type TargetTrackingScaling \ + --target-tracking-scaling-policy-configuration file://scaling-policy.json +``` + +### 3. Use Reserved Instances for EC2 + +For long-running deployments, consider Reserved Instances to reduce costs. + +## Troubleshooting + +### Common Issues + +1. **Service won't start**: Check CloudWatch logs for container errors +2. **Can't access application**: Verify security group rules and target group health +3. **SSL certificate issues**: Ensure DNS validation is complete +4. **Vault connection errors**: Check network connectivity between containers + +### Debugging Commands + +```bash +# Check ECS service status +aws ecs describe-services \ + --cluster sup3rs3cretmes5age-cluster \ + --services sup3rs3cretmes5age-service + +# View logs +aws logs tail /ecs/sup3rs3cretmes5age --follow + +# Check target group health +aws elbv2 describe-target-health \ + --target-group-arn $TARGET_GROUP_ARN + +# Test internal connectivity +aws ecs execute-command \ + --cluster sup3rs3cretmes5age-cluster \ + --task \ + --container supersecret \ + --interactive \ + --command "/bin/sh" +``` + +### Support Resources + +- [AWS ECS Documentation](https://docs.aws.amazon.com/ecs/) +- [HashiCorp Vault Documentation](https://www.vaultproject.io/docs) +- [Application Load Balancer Documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/) + +--- + +This guide provides multiple deployment options on AWS. Choose the option that best fits your team's expertise and requirements. For production deployments, we recommend Option 1 (ECS with Fargate) for its balance of simplicity, scalability, and cost-effectiveness. \ No newline at end of file diff --git a/README.md b/README.md index dd56719..f4a9b18 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,275 @@ # sup3rS3cretMes5age -A simple, secure self-destructing message service, using HashiCorp Vault product as a backend. +[![Go Version](https://img.shields.io/github/go-mod/go-version/algolia/sup3rS3cretMes5age.svg)](https://golang.org/) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![CircleCI](https://img.shields.io/circleci/build/github/algolia/sup3rS3cretMes5age/master)](https://circleci.com/gh/algolia/sup3rS3cretMes5age) +[![Go Report Card](https://goreportcard.com/badge/github.com/algolia/sup3rS3cretMes5age)](https://goreportcard.com/report/github.com/algolia/sup3rS3cretMes5age) + +[![Awesome F/OSS](https://awsmfoss.com/content/images/2024/02/awsm-foss-badge.600x128.rounded.png)](https://awsmfoss.com/sup3rs3cretmes5age) + +A simple, secure, **self-destructing message service** that uses HashiCorp Vault as a backend for temporary secret storage. Share sensitive information with confidence knowing it will be automatically deleted after being read once. ![self-destruct](https://media.giphy.com/media/LBlyAAFJ71eMw/giphy.gif) +> 🔐 **Security First**: Messages are stored in Vault's cubbyhole backend with one-time tokens and automatic expiration. + Read more about the reasoning behind this project in the [relevant blog post](https://blog.algolia.com/secure-tool-for-one-time-self-destructing-messages/). -Now using [Let's Encrypt](https://letsencrypt.org/) for simple and free SSL certs! +## ✨ Features + +- **🔥 Self-Destructing Messages**: Messages are automatically deleted after first read +- **⏰ Configurable TTL**: Set custom expiration times (default 48h, max 7 days) +- **📎 File Upload Support**: Share files up to 50MB with base64 encoding +- **🔐 Vault-Backed Security**: Uses HashiCorp Vault's cubbyhole for tamper-proof storage +- **🎫 One-Time Tokens**: Vault tokens with exactly 2 uses (create + retrieve) +- **🚦 Rate Limiting**: Built-in protection (10 requests/second) +- **🔒 TLS/HTTPS Support**: + - Automatic TLS via [Let's Encrypt](https://letsencrypt.org/) + - Manual certificate configuration + - HTTP to HTTPS redirection +- **🌐 No External Dependencies**: All assets self-hosted for privacy +- **📦 Lightweight**: Only 8.9KB JavaScript (no jQuery) +- **🐳 Docker Ready**: Multi-platform images (amd64, arm64) with SBOM +- **☸️ Kubernetes Support**: Helm chart included +- **🖥️ CLI Integration**: Shell functions for Bash, Zsh, and Fish + +## 📋 Table of Contents + +- [Features](#-features) +- [Frontend Dependencies](#frontend-dependencies) +- [Quick Start](#-quick-start) +- [Deployment](#deployment) +- [Configuration](#configuration-options) +- [Command Line Usage](#command-line-usage) +- [Helm Chart](#helm) +- [API Reference](#-api-reference) +- [Development](#-development) +- [Contributing](#contributing) +- [License](#license) ## Frontend Dependencies -The web interface is built with modern vanilla JavaScript and has minimal external dependencies: +The web interface is built with modern **vanilla JavaScript** and has minimal external dependencies: + +| Dependency | Size | Purpose | +|------------|------|----------| +| ClipboardJS v2.0.11 | 8.9KB | Copy to clipboard functionality | +| Montserrat Font | 46KB | Self-hosted typography | +| Custom CSS | 2.3KB | Application styling | + +✅ **No external CDNs or tracking** - All dependencies are self-hosted for privacy and security. + +📦 **Total JavaScript bundle size**: 8.9KB (previously 98KB with jQuery) + +## 🚀 Quick Start + +Get up and running in less than 2 minutes: + +```bash +# Clone the repository +git clone https://github.com/algolia/sup3rS3cretMes5age.git +cd sup3rS3cretMes5age + +# Start with Docker Compose (recommended) +make run + +# Access the application +open http://localhost:8082 +``` -- **ClipboardJS v2.0.11** (8.9KB) - Copy to clipboard functionality -- **Montserrat Font** (46KB) - Self-hosted typography -- **Custom CSS** - Application styling +The service will start with: +- **Application**: http://localhost:8082 +- **Vault dev server**: In-memory storage with token `supersecret` -**No external CDNs or tracking:** All dependencies are self-hosted for privacy and security. +### Alternative: Local Build -**Total JavaScript bundle size:** 8.9KB (previously 98KB with jQuery) +```bash +# Start Vault dev server +docker run -d --name vault-dev -p 8200:8200 \ + -e VAULT_DEV_ROOT_TOKEN_ID=supersecret \ + hashicorp/vault:latest + +# Build and run the application +go build -o sup3rs3cret cmd/sup3rS3cretMes5age/main.go +VAULT_ADDR=http://localhost:8200 \ +VAULT_TOKEN=supersecret \ +SUPERSECRETMESSAGE_HTTP_BINDING_ADDRESS=":8080" \ +./sup3rs3cret +``` ## Deployment -### Testing it locally +### Local Development -You can just run `docker-compose up -f deploy/docker-compose.yml --build` or run `make build`: it will build the Docker image and then run it alongside a standalone Vault server. +#### Using Make (Recommended) -By default, the `deploy/docker-compose.yml` is configured to run the webapp on port 8082 in cleartext HTTP (so you can access it on [http://localhost:8082](http://localhost:8082)). +```bash +make run # Start services (Vault + App) +make logs # View logs +make stop # Stop services +make clean # Remove containers +``` -Optionally, you can modify the `deploy/docker-compose.yml` and tweak the options (enable HTTPS, disable HTTP or enable redirection to HTTPS, etc.). See [Configuration options](#configuration-options). +#### Using Docker Compose Directly + +```bash +docker compose -f deploy/docker-compose.yml up --build -d +``` + +By default, the application runs on **port 8082** in HTTP mode: [http://localhost:8082](http://localhost:8082) + +💡 You can modify `deploy/docker-compose.yml` to enable HTTPS, HTTP redirection, or change ports. See [Configuration options](#configuration-options). ### Production Deployment -We recommend deploying the project via **Docker** and a **container orchestration tool**: +The image is available at: +- **Docker Hub**: `algolia/supersecretmessage:latest` +- **Platforms**: linux/amd64, linux/arm64 + +#### Docker Image -* Build the Docker image using the provided `Dockerfile` or run `make image` -* Host it in a Docker registry ([Docker Hub](https://hub.docker.com/), [AWS ECR](https://aws.amazon.com/ecr/), etc.) -* Deploy the image (alongside with a standalone Vault server) using a container orchestration tool ([Kubernetes](https://kubernetes.io/), [Docker Swarm](https://docs.docker.com/engine/swarm/), [AWS ECS](https://aws.amazon.com/ecs/), etc.) +Build multi-platform images with SBOM and provenance attestations: + +```bash +# Build for multiple architectures +make image +# Builds: linux/amd64, linux/arm64 with SBOM and provenance +``` + +#### AWS Deployment + +For detailed step-by-step instructions on deploying to AWS, see our comprehensive [AWS Deployment Guide](AWS_DEPLOYMENT.md). The guide covers: + +- **ECS with Fargate** (recommended) - Serverless containers with Application Load Balancer +- **EKS (Kubernetes)** - Using the provided Helm chart on Amazon EKS +- **EC2 with Docker** - Simple deployment using Docker Compose You can read the [configuration examples](#configuration-examples) below. -### Security notice +#### Deployment Platforms + +Deploy using your preferred orchestration tool: + +| Platform | Documentation | +|----------|---------------| +| Kubernetes | See [Helm Chart](#helm) below | +| Docker Swarm | Use the provided `docker-compose.yml` | +| AWS ECS | Use the Docker image with ECS task definition | + +**Important**: Deploy alongside a production Vault server. Configure via environment variables: +- `VAULT_ADDR`: Your Vault server URL +- `VAULT_TOKEN`: Vault authentication token + +See [configuration examples](#configuration-examples) below. -Whatever deployment method you choose, **you should always run this behind SSL/TLS**, otherwise secrets will be sent _unencrypted_! +### 🔒 Security Notice -Depending on your infrastructure/deployment, you can have **TLS termination** either _inside the container_ (see [Configuration examples - TLS](#tls)), or _before_ e.g. at a load balancer/reverse proxy in front of the service. -It is interesting to have TLS termination before the container so you don't have to manage the certificate/key there, but **make sure the network** between your TLS termination point and your container **is secure**. +> ⚠️ **Critical**: Always run this service behind SSL/TLS in production. Secrets sent over HTTP are vulnerable to interception! + +#### TLS Termination Options + +**Option 1: Inside the Container** (Recommended for simplicity) +- Configure via environment variables +- Automatic Let's Encrypt certificates +- See [Configuration examples - TLS](#tls) + +**Option 2: External Load Balancer/Reverse Proxy** +- Simpler certificate management +- Offload TLS processing +- **Ensure secure network** between proxy and container +- Examples: AWS ALB, Nginx, Traefik, Cloudflare + +#### Security Best Practices + +- ✅ Use HTTPS/TLS in production +- ✅ Use a production Vault server (not dev mode) +- ✅ Rotate Vault tokens regularly +- ✅ Enable rate limiting (built-in: 10 req/s) +- ✅ Monitor Vault audit logs +- ✅ Use strong Vault policies +- ✅ Keep dependencies updated ## Helm -For full documentation for this chart, please see the [README](https://github.com/algolia/sup3rS3cretMes5age/blob/master/deployments/charts/README.md) +Deploy to Kubernetes using the included Helm chart: + +```bash +helm install supersecret ./deploy/charts/supersecretmessage \ + --set config.vault.address=http://vault.default.svc.cluster.local:8200 \ + --set config.vault.token_secret.name=vault-token +``` + +**Chart Details**: +- Chart Version: 0.1.0 +- App Version: 0.2.5 +- Includes: Deployment, Service, Ingress, HPA, ServiceAccount + +For full documentation, see the [Helm Chart README](deploy/charts/README.md) + +## 📡 API Reference + +### Create Secret Message + +**Endpoint**: `POST /secret` + +**Content-Type**: `multipart/form-data` + +**Parameters**: +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `msg` | string | Yes | The secret message content | +| `ttl` | string | No | Time-to-live (default: 48h, max: 168h) | +| `file` | file | No | File to upload (max 50MB) | + +**Response**: +```json +{ + "token": "s.abc123def456", + "filetoken": "s.xyz789uvw012", // If file uploaded + "filename": "secret.pdf" // If file uploaded +} +``` + +**Example**: +```bash +# Text message +curl -X POST -F 'msg=This is a secret' http://localhost:8082/secret + +# With custom TTL +curl -X POST -F 'msg=Short-lived secret' -F 'ttl=1h' http://localhost:8082/secret + +# With file +curl -X POST -F 'msg=Check this file' -F 'file=@secret.pdf' http://localhost:8082/secret +``` + +### Retrieve Secret Message + +**Endpoint**: `GET /secret?token=` + +**Parameters**: +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `token` | string | Yes | The token from POST response | + +**Response**: +```json +{ + "msg": "This is a secret" +} +``` + +**Example**: +```bash +curl "http://localhost:8082/secret?token=s.abc123def456" +``` + +⚠️ **Note**: After retrieval, the message and token are permanently deleted. Second attempts will fail. + +### Health Check + +**Endpoint**: `GET /health` + +**Response**: `OK` (HTTP 200) ## Command Line Usage @@ -158,18 +378,119 @@ SUPERSECRETMESSAGE_TLS_CERT_FILEPATH=/mnt/ssl/cert_secrets.example.com.pem SUPERSECRETMESSAGE_TLS_CERT_KEY_FILEPATH=/mnt/ssl/key_secrets.example.com.pem ``` -## Screenshot +## 📸 Screenshots + +### Message Creation Interface +![supersecretmsg](https://github.com/user-attachments/assets/0ada574b-99e4-4562-aea4-a1868d6ca0d8) + +*Clean, intuitive interface for creating self-destructing messages with optional file uploads and custom TTL.* + +### Message Retrieval Interface +![supersecretmsg](https://github.com/user-attachments/assets/6d0c455f-00ca-430e-bc8c-e721e071843a") + +*Simple, secure interface for viewing self-destructing messages that are permanently deleted upon retrieval.* + +## 🛠️ Development + +### Prerequisites + +- Go 1.25.1 or later +- Docker (for Vault dev server) +- Make (optional, for convenience) + +### Setup + +```bash +# Clone the repository +git clone https://github.com/algolia/sup3rS3cretMes5age.git +cd sup3rS3cretMes5age + +# Download dependencies +go mod download + +# Build the binary +go build -o sup3rs3cret cmd/sup3rS3cretMes5age/main.go +``` + +### Running Tests -![supersecretmsg](https://user-images.githubusercontent.com/357094/29357449-e9268adc-8277-11e7-8fef-b1eabfe62444.png) +```bash +# Run all tests +make test + +# Or directly with go +go test ./... -v +``` + +### Code Quality + +```bash +# Format code +gofmt -s -w . + +# Lint +golangci-lint run --timeout 300s + +# Static analysis +go vet ./... +``` + +### Project Structure + +``` +. +├── cmd/sup3rS3cretMes5age/ # Application entry point +│ └── main.go # (23 lines) +├── internal/ # Core business logic +│ ├── config.go # Configuration (77 lines) +│ ├── handlers.go # HTTP handlers (88 lines) +│ ├── server.go # Server setup (94 lines) +│ └── vault.go # Vault integration (174 lines) +├── web/static/ # Frontend assets +│ ├── index.html # Message creation page +│ ├── getmsg.html # Message retrieval page +│ ├── application.css # Styling +│ └── clipboard-2.0.11.min.js +├── deploy/ # Deployment configs +│ ├── Dockerfile # Multi-stage build +│ ├── docker-compose.yml # Local dev stack +│ └── charts/ # Helm chart +└── Makefile # Build automation +``` + +**Total Code**: 609 lines of Go across 7 files ## Contributing -Pull requests are very welcome! -Please consider that they will be reviewed by our team at Algolia. +Contributions are welcome! 🎉 + +### How to Contribute + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +### Guidelines + +- Write tests for new features +- Follow existing code style +- Update documentation as needed +- Ensure all tests pass (`make test`) +- Run linters (`golangci-lint run`) + +All pull requests will be reviewed by the Algolia team. + +## License +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -## Thanks +## 🙏 Acknowledgments -This project is heavaily depandent on the amazing work of the [Echo Go Web Framework](https://github.com/labstack/echo) and Hashicorp Vault. +This project is built on the shoulders of giants: -[![This project is certified Awesome F/OSS](https://awsmfoss.com/content/images/2024/02/awsm-foss-badge.600x128.rounded.png)](https://awsmfoss.com/sup3rs3cretmes5age) +- **[HashiCorp Vault](https://www.vaultproject.io/)** - Secure secret storage backend +- **[Echo](https://echo.labstack.com/)** - High performance Go web framework +- **[Let's Encrypt](https://letsencrypt.org/)** - Free SSL/TLS certificates +- **[ClipboardJS](https://clipboardjs.com/)** - Modern clipboard functionality diff --git a/cmd/sup3rS3cretMes5age/main.go b/cmd/sup3rS3cretMes5age/main.go index 4af00e8..bb304e8 100644 --- a/cmd/sup3rS3cretMes5age/main.go +++ b/cmd/sup3rS3cretMes5age/main.go @@ -1,3 +1,5 @@ +// Package main provides the entry point for the sup3rS3cretMes5age application, +// a secure self-destructing message service using HashiCorp Vault as a backend. package main import ( @@ -8,6 +10,7 @@ import ( "github.com/algolia/sup3rS3cretMes5age/internal" ) +// version holds the application version string, injected at build time via ldflags. var version = "" func main() { diff --git a/internal/config.go b/internal/config.go index c7e7193..98b996f 100644 --- a/internal/config.go +++ b/internal/config.go @@ -1,3 +1,5 @@ +// Package internal contains the core business logic for the sup3rS3cretMes5age application, +// including configuration management, HTTP handlers, server setup, and Vault integration. package internal import ( @@ -6,24 +8,46 @@ import ( "strings" ) +// conf holds the application configuration settings loaded from environment variables. +// It includes HTTP/HTTPS binding addresses, TLS configuration, and Vault storage prefix. type conf struct { - HttpBindingAddress string - HttpsBindingAddress string + // HttpBindingAddress is the HTTP server binding address (e.g., ":8080"). + HttpBindingAddress string + // HttpsBindingAddress is the HTTPS server binding address (e.g., ":443"). + HttpsBindingAddress string + // HttpsRedirectEnabled determines whether HTTP requests should redirect to HTTPS. HttpsRedirectEnabled bool - TLSAutoDomain string - TLSCertFilepath string - TLSCertKeyFilepath string - VaultPrefix string + // TLSAutoDomain is the domain for automatic Let's Encrypt TLS certificate generation. + TLSAutoDomain string + // TLSCertFilepath is the path to a manual TLS certificate file. + TLSCertFilepath string + // TLSCertKeyFilepath is the path to a manual TLS certificate key file. + TLSCertKeyFilepath string + // VaultPrefix is the Vault storage path prefix (defaults to "cubbyhole/"). + VaultPrefix string } -const HttpBindingAddressVarenv = "SUPERSECRETMESSAGE_HTTP_BINDING_ADDRESS" -const HttpsBindingAddressVarenv = "SUPERSECRETMESSAGE_HTTPS_BINDING_ADDRESS" -const HttpsRedirectEnabledVarenv = "SUPERSECRETMESSAGE_HTTPS_REDIRECT_ENABLED" -const TLSAutoDomainVarenv = "SUPERSECRETMESSAGE_TLS_AUTO_DOMAIN" -const TLSCertFilepathVarenv = "SUPERSECRETMESSAGE_TLS_CERT_FILEPATH" -const TLSCertKeyFilepathVarenv = "SUPERSECRETMESSAGE_TLS_CERT_KEY_FILEPATH" -const VaultPrefixenv = "SUPERSECRETMESSAGE_VAULT_PREFIX" +// Environment variable names for application configuration. +const ( + // HttpBindingAddressVarenv is the environment variable for HTTP binding address. + HttpBindingAddressVarenv = "SUPERSECRETMESSAGE_HTTP_BINDING_ADDRESS" + // HttpsBindingAddressVarenv is the environment variable for HTTPS binding address. + HttpsBindingAddressVarenv = "SUPERSECRETMESSAGE_HTTPS_BINDING_ADDRESS" + // HttpsRedirectEnabledVarenv is the environment variable to enable HTTPS redirect. + HttpsRedirectEnabledVarenv = "SUPERSECRETMESSAGE_HTTPS_REDIRECT_ENABLED" + // TLSAutoDomainVarenv is the environment variable for automatic TLS domain. + TLSAutoDomainVarenv = "SUPERSECRETMESSAGE_TLS_AUTO_DOMAIN" + // TLSCertFilepathVarenv is the environment variable for manual TLS certificate path. + TLSCertFilepathVarenv = "SUPERSECRETMESSAGE_TLS_CERT_FILEPATH" + // TLSCertKeyFilepathVarenv is the environment variable for manual TLS key path. + TLSCertKeyFilepathVarenv = "SUPERSECRETMESSAGE_TLS_CERT_KEY_FILEPATH" + // VaultPrefixenv is the environment variable for Vault storage prefix. + VaultPrefixenv = "SUPERSECRETMESSAGE_VAULT_PREFIX" +) +// LoadConfig loads and validates application configuration from environment variables. +// It validates TLS configuration mutual exclusivity, ensures required bindings are set, +// and sets default values where appropriate. Exits with fatal error on invalid configuration. func LoadConfig() conf { var cnf conf diff --git a/internal/handlers.go b/internal/handlers.go index 83c24b2..d5ef701 100644 --- a/internal/handlers.go +++ b/internal/handlers.go @@ -8,28 +8,43 @@ import ( "github.com/labstack/echo/v4" ) +// TokenResponse represents the API response when creating a new secret message. +// It includes a token for retrieving the message, and optional file token and name +// if a file was uploaded alongside the message. type TokenResponse struct { - Token string `json:"token"` + // Token is the unique identifier for retrieving the secret message. + Token string `json:"token"` + // FileToken is the unique identifier for retrieving an uploaded file (optional). FileToken string `json:"filetoken,omitempty"` - FileName string `json:"filename,omitempty"` + // FileName is the original name of the uploaded file (optional). + FileName string `json:"filename,omitempty"` } +// MsgResponse represents the API response when retrieving a secret message. type MsgResponse struct { + // Msg is the secret message content retrieved from Vault. Msg string `json:"msg"` } +// SecretHandlers provides HTTP handler methods for creating and retrieving secret messages. type SecretHandlers struct { + // store is the backend storage implementation (Vault) for secret messages. store SecretMsgStorer } +// newSecretHandlers creates a new SecretHandlers instance with the provided storage backend. func newSecretHandlers(s SecretMsgStorer) *SecretHandlers { return &SecretHandlers{s} } +// CreateMsgHandler handles POST requests to create a new self-destructing secret message. +// It accepts form data with 'msg' (required), 'ttl' (optional time-to-live), and 'file' (optional file upload). +// Files are base64 encoded before storage. Maximum file size is 50MB (enforced by middleware). +// Returns a JSON response with token(s) for retrieving the message and/or file. func (s SecretHandlers) CreateMsgHandler(ctx echo.Context) error { var tr TokenResponse - //Get TTL (if any) + // Get TTL (if any) ttl := ctx.FormValue("ttl") // Upload file if any @@ -68,6 +83,9 @@ func (s SecretHandlers) CreateMsgHandler(ctx echo.Context) error { return ctx.JSON(http.StatusOK, tr) } +// GetMsgHandler handles GET requests to retrieve a self-destructing secret message. +// Accepts a 'token' query parameter. The message is deleted from Vault after retrieval, +// making it accessible only once. Returns a JSON response with the message content. func (s SecretHandlers) GetMsgHandler(ctx echo.Context) error { m, err := s.store.Get(ctx.QueryParam("token")) if err != nil { @@ -79,10 +97,13 @@ func (s SecretHandlers) GetMsgHandler(ctx echo.Context) error { return ctx.JSON(http.StatusOK, r) } +// healthHandler provides a simple health check endpoint. +// Returns HTTP 200 OK when the application is running. func healthHandler(ctx echo.Context) error { return ctx.String(http.StatusOK, http.StatusText(http.StatusOK)) } +// redirectHandler redirects the root path to the message creation page. func redirectHandler(ctx echo.Context) error { return ctx.Redirect(http.StatusPermanentRedirect, "/msg") } diff --git a/internal/server.go b/internal/server.go index 397f980..92d4255 100644 --- a/internal/server.go +++ b/internal/server.go @@ -10,6 +10,11 @@ import ( "golang.org/x/crypto/acme/autocert" ) +// Serve starts the HTTP/HTTPS server with the provided configuration. +// It sets up the Echo web framework with middleware (rate limiting, logging, security), +// configures TLS (automatic via Let's Encrypt or manual), and registers all HTTP routes. +// Vault connection details are read from VAULT_ADDR and VAULT_TOKEN environment variables. +// The server runs until terminated or encounters a fatal error. func Serve(cnf conf) { // Vault address and token are taken from VAULT_ADDR and VAULT_TOKEN environment variables handlers := newSecretHandlers(newVault("", cnf.VaultPrefix, "")) diff --git a/internal/vault.go b/internal/vault.go index 7e2a6f8..ae80749 100644 --- a/internal/vault.go +++ b/internal/vault.go @@ -8,18 +8,29 @@ import ( "github.com/hashicorp/vault/api" ) +// SecretMsgStorer defines the interface for storing and retrieving self-destructing messages. +// Implementations must ensure messages are deleted after first retrieval (one-time access). type SecretMsgStorer interface { + // Store saves a message with the specified TTL and returns a unique retrieval token. Store(string, ttl string) (token string, err error) + // Get retrieves a message by token and deletes it from storage (one-time read). Get(token string) (msg string, err error) } +// vault implements SecretMsgStorer using HashiCorp Vault's cubbyhole backend. +// It manages one-time tokens and automatic token renewal for secure message storage. type vault struct { + // address is the Vault server URL (read from VAULT_ADDR if empty). address string - prefix string - token string + // prefix is the Vault storage path prefix (e.g., "cubbyhole/"). + prefix string + // token is the Vault authentication token (read from VAULT_TOKEN if empty). + token string } -// NewVault creates a vault client to talk with underline vault server +// newVault creates a new vault client and starts a background goroutine for token renewal. +// If address or token are empty, they will be read from VAULT_ADDR and VAULT_TOKEN +// environment variables respectively. The prefix determines the Vault storage path. func newVault(address string, prefix string, token string) vault { v := vault{address, prefix, token} @@ -27,6 +38,10 @@ func newVault(address string, prefix string, token string) vault { return v } +// Store saves a message to Vault with the specified time-to-live (TTL). +// Default TTL is 48 hours if not specified. Maximum TTL is 168 hours (7 days). +// Returns a unique one-time token for retrieving the message. +// The token can be used exactly twice: once to store and once to retrieve. func (v vault) Store(msg string, ttl string) (token string, err error) { // Default TTL if ttl == "" { @@ -55,6 +70,9 @@ func (v vault) Store(msg string, ttl string) (token string, err error) { return t, nil } +// createOneTimeToken creates a non-renewable Vault token with exactly 2 uses. +// The token is used once to write the message and once to read it, ensuring +// one-time access. The token automatically expires after the specified TTL. func (v vault) createOneTimeToken(ttl string) (string, error) { c, err := v.newVaultClient() if err != nil { @@ -76,6 +94,9 @@ func (v vault) createOneTimeToken(ttl string) (string, error) { return s.Auth.ClientToken, nil } +// newVaultClient creates a new Vault API client with the configured address and token. +// If the vault address is empty, it defaults to using the VAULT_ADDR environment variable. +// If the vault token is empty, it defaults to using the VAULT_TOKEN environment variable. func (v vault) newVaultClient() (*api.Client, error) { c, err := api.NewClient(api.DefaultConfig()) if err != nil { @@ -98,6 +119,9 @@ func (v vault) newVaultClient() (*api.Client, error) { return c, nil } +// writeMsgToVault writes a message to Vault using the provided one-time token. +// The message is stored at the path: //. +// This consumes the first use of the two-use token. func (v vault) writeMsgToVault(token, msg string) error { c, err := v.newVaultClientWithToken(token) if err != nil { @@ -111,6 +135,9 @@ func (v vault) writeMsgToVault(token, msg string) error { return err } +// Get retrieves and deletes a message from Vault using the provided token. +// This consumes the second (final) use of the two-use token, automatically +// deleting both the message and the token from Vault, ensuring one-time access. func (v vault) Get(token string) (msg string, err error) { c, err := v.newVaultClientWithToken(token) if err != nil { @@ -124,6 +151,8 @@ func (v vault) Get(token string) (msg string, err error) { return r.Data["msg"].(string), nil } +// newVaultClientWithToken creates a Vault client authenticated with a specific token. +// Used for one-time token operations when storing and retrieving messages. func (v vault) newVaultClientWithToken(token string) (*api.Client, error) { c, err := v.newVaultClient() if err != nil { @@ -133,6 +162,9 @@ func (v vault) newVaultClientWithToken(token string) (*api.Client, error) { return c, nil } +// newVaultClientToRenewToken runs in a background goroutine to automatically renew +// the main Vault authentication token before it expires. This ensures continuous +// operation of the service without manual token refresh. func (v vault) newVaultClientToRenewToken() { c, err := v.newVaultClient() if err != nil {