Skip to content

Verify

The Azure and AWS offerings have different runtimes and verification paths. Use SSH, Docker, nginx, and cloud-init checks only for the Azure full-origin. The AWS offering has no SSH-accessible host and must be verified through ECS, ELB, CloudWatch, S3, network reachability, and Terraform.

Run these checks after allowing 5—10 minutes for cloud-init to complete.

Terminal window
ORIGIN_IP=$(terraform output -raw public_ip)
curl -sf "http://${ORIGIN_IP}/health" | jq .

Expected:

{
"status": "healthy",
"component": "origin-server",
"applications": ["juice-shop", "dvwa", "vampi", "httpbin", "whoami", "csd-demo", "dvga", "restaurant", "crapi"]
}
Terminal window
# Default landing page
curl -sf "http://${ORIGIN_IP}/" -o /dev/null -w "Landing: %{http_code}\n"
# Juice Shop
curl -sf "http://${ORIGIN_IP}/juice-shop/" -o /dev/null -w "Juice Shop: %{http_code}\n"
# DVWA
curl -sf "http://${ORIGIN_IP}/dvwa/" -o /dev/null -w "DVWA: %{http_code}\n"
# VAmPI
curl -sf "http://${ORIGIN_IP}/vampi/" -o /dev/null -w "VAmPI: %{http_code}\n"
# httpbin
curl -sf "http://${ORIGIN_IP}/httpbin/get" -o /dev/null -w "httpbin: %{http_code}\n"
# whoami
curl -sf "http://${ORIGIN_IP}/whoami/" -o /dev/null -w "whoami: %{http_code}\n"
# DVGA
curl -sf "http://${ORIGIN_IP}/dvga/" -o /dev/null -w "DVGA: %{http_code}\n"
# RESTaurant
curl -sf "http://${ORIGIN_IP}/restaurant/docs" -o /dev/null -w "RESTaurant: %{http_code}\n"
# crAPI (port 8888)
curl -sf "http://${ORIGIN_IP}:8888" -o /dev/null -w "crAPI: %{http_code}\n"

All should return 200 (DVWA returns 302 redirect to login).

The repository includes a 39-point smoke test suite:

Terminal window
./tests/smoke-test.sh ${ORIGIN_IP}

This validates all 9 applications, health endpoints, VAmPI registration/login, CSD Demo exfil round-trip, DVGA GraphQL, RESTaurant Swagger, crAPI on port 8888, nginx gzip, and version hiding.

SSH into the VM and verify all 41 containers are running:

Terminal window
ssh azureuser@${ORIGIN_IP} "sudo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'" | sort

Expected output (41 containers):

NAMES STATUS PORTS
crapi-community Up X minutes
crapi-identity Up X minutes
crapi-mailhog Up X minutes 0.0.0.0:18025->8025/tcp
crapi-mongo Up X minutes 27017/tcp
crapi-postgres Up X minutes 5432/tcp
crapi-web Up X minutes 0.0.0.0:8888->80/tcp
crapi-workshop Up X minutes
csd-demo-1 Up X minutes 127.0.0.1:5001->5001/tcp
csd-demo-2 Up X minutes 127.0.0.1:5002->5001/tcp
csd-demo-3 Up X minutes 127.0.0.1:5003->5001/tcp
csd-demo-4 Up X minutes 127.0.0.1:5004->5001/tcp
dvga-1 Up X minutes 127.0.0.1:5201->5013/tcp
dvga-2 Up X minutes 127.0.0.1:5202->5013/tcp
dvga-3 Up X minutes 127.0.0.1:5203->5013/tcp
dvga-4 Up X minutes 127.0.0.1:5204->5013/tcp
dvwa-1 Up X minutes 127.0.0.1:8101->80/tcp
dvwa-2 Up X minutes 127.0.0.1:8102->80/tcp
dvwa-3 Up X minutes 127.0.0.1:8103->80/tcp
dvwa-4 Up X minutes 127.0.0.1:8104->80/tcp
dvwa-db Up X minutes 3306/tcp
httpbin-1 Up X minutes 127.0.0.1:8201->80/tcp
httpbin-2 Up X minutes 127.0.0.1:8202->80/tcp
httpbin-3 Up X minutes 127.0.0.1:8203->80/tcp
httpbin-4 Up X minutes 127.0.0.1:8204->80/tcp
juice-shop-1 Up X minutes 127.0.0.1:3001->3000/tcp
juice-shop-2 Up X minutes 127.0.0.1:3002->3000/tcp
juice-shop-3 Up X minutes 127.0.0.1:3003->3000/tcp
juice-shop-4 Up X minutes 127.0.0.1:3004->3000/tcp
restaurant-1 Up X minutes 127.0.0.1:8301->8080/tcp
restaurant-2 Up X minutes 127.0.0.1:8302->8080/tcp
restaurant-3 Up X minutes 127.0.0.1:8303->8080/tcp
restaurant-4 Up X minutes 127.0.0.1:8304->8080/tcp
restaurant-db Up X minutes 5432/tcp
vampi-1 Up X minutes 127.0.0.1:5101->5000/tcp
vampi-2 Up X minutes 127.0.0.1:5102->5000/tcp
vampi-3 Up X minutes 127.0.0.1:5103->5000/tcp
vampi-4 Up X minutes 127.0.0.1:5104->5000/tcp
whoami-1 Up X minutes 127.0.0.1:8082->80/tcp
whoami-2 Up X minutes 127.0.0.1:8083->80/tcp
whoami-3 Up X minutes 127.0.0.1:8084->80/tcp
whoami-4 Up X minutes 127.0.0.1:8085->80/tcp
Terminal window
ssh azureuser@${ORIGIN_IP} "sudo nginx -t && sudo systemctl status nginx --no-pager"

Run these checks from the Terraform root that consumes terraform/modules/aws-juice-shop. Expose the module outputs from that root, then capture them:

output "origin_url" {
value = module.juice_shop.origin_url
}
output "ecs_cluster_id" {
value = module.juice_shop.ecs_cluster_id
}
output "ecs_service_id" {
value = module.juice_shop.ecs_service_id
}
output "target_group_arn" {
value = module.juice_shop.target_group_arn
}
Terminal window
ORIGIN=$(terraform output -raw origin_url)
ECS_CLUSTER=$(terraform output -raw ecs_cluster_id)
ECS_SERVICE=$(terraform output -raw ecs_service_id)
TARGET_GROUP_ARN=$(terraform output -raw target_group_arn)

The AWS origin serves Juice Shop at ${ORIGIN}/; /juice-shop/, DVGA, RESTaurant, crAPI, and every other non-Juice-Shop endpoint in this documentation require the Azure full-origin.

Before running any AWS curl command, choose an execution location that can reach the configured ALB:

  • Internal ALB (default): run from a host or runner with private DNS resolution and an implemented route into the VPC, such as VPN, Direct Connect, peering, or an F5 Distributed Cloud connected network. If F5 Distributed Cloud is the caller, its private origin connectivity must already be implemented and verified.
  • Internet-facing ALB: run from a source included in allowed_ingress_cidrs. The ALB subnets must have routes to an Internet Gateway; selecting subnets in the correct VPC and Availability Zones does not make them public.

The module validates VPC and Availability Zone placement only. It does not create or validate subnet routes, gateways, VPNs, Direct Connect, peering, or F5 Distributed Cloud private connectivity.

Terminal window
aws ecs wait services-stable --cluster "${ECS_CLUSTER}" --services "${ECS_SERVICE}"
aws ecs describe-services --cluster "${ECS_CLUSTER}" --services "${ECS_SERVICE}" \
--query 'services[0].{desired:desiredCount,running:runningCount,pending:pendingCount,deployments:deployments[*].rolloutState}'
aws elbv2 describe-target-health --target-group-arn "${TARGET_GROUP_ARN}" \
--query 'TargetHealthDescriptions[].TargetHealth.{state:State,reason:Reason,description:Description}'

The service must reach steady state with the desired task count running and every registered target reporting healthy.

The module log group is /ecs/<module-name>, where <module-name> is the module’s name input.

Terminal window
AWS_LOG_GROUP="/ecs/<module-name>"
aws logs tail "${AWS_LOG_GROUP}" --since 10m

Confirm recent juice-shop stream events exist and that there are no repeated startup failures. Do not use SSH or Docker commands: Fargate does not provide the Azure VM’s container-host access model.

The caller supplies the S3 bucket and optional prefix. From the execution location selected above, generate one authorized request, then confirm that ELB log objects arrive under the regional AWSLogs/<account-id>/elasticloadbalancing/ hierarchy beneath that prefix:

Terminal window
curl -sf "${ORIGIN}/" -o /dev/null -w "Juice Shop: %{http_code}\n"
aws s3api list-objects-v2 \
--bucket "<caller-managed-alb-log-bucket>" \
--prefix "<configured-prefix>/AWSLogs/" \
--query 'reverse(sort_by(Contents,&LastModified))[:5].[Key,LastModified,Size]'

Delivery is asynchronous; retry after several minutes before treating an empty result as a failure.

From the applicable execution location described above, test ${ORIGIN}/ from an address included in allowed_ingress_cidrs; expect HTTP 200. Test from a controlled address outside those CIDRs; the connection must fail.

Do not make the ALB public to work around missing routing. If public_exposure = true is an explicit requirement, restrict allowed_ingress_cidrs to approved sources and verify that each selected ALB subnet routes through an Internet Gateway. The F5 Distributed Cloud-to-ALB HTTP hop is acceptable only inside the documented trusted routing domain; use a different origin design if interception protection cannot be established.

Terminal window
terraform plan -detailed-exitcode

Exit code 0 confirms no changes. Exit code 2 means drift or unapplied configuration and must be reviewed; exit code 1 is an error.

If endpoints return 502 or connection refused, cloud-init may still be provisioning:

Terminal window
ssh azureuser@${ORIGIN_IP} "cloud-init status"
  • status: running — still provisioning, wait a few minutes
  • status: done — provisioning complete, check Docker containers
  • status: error — check logs below
Terminal window
ssh azureuser@${ORIGIN_IP} "sudo tail -100 /var/log/cloud-init-output.log"
Terminal window
# Check container logs (each app has 4 instances: -1 through -4)
ssh azureuser@${ORIGIN_IP} "sudo docker logs juice-shop-1 2>&1 | tail -20"
ssh azureuser@${ORIGIN_IP} "sudo docker logs dvwa-1 2>&1 | tail -20"
ssh azureuser@${ORIGIN_IP} "sudo docker logs vampi-1 2>&1 | tail -20"
ssh azureuser@${ORIGIN_IP} "sudo docker logs httpbin-1 2>&1 | tail -20"
ssh azureuser@${ORIGIN_IP} "sudo docker logs csd-demo-1 2>&1 | tail -20"
ssh azureuser@${ORIGIN_IP} "sudo docker logs dvga-1 2>&1 | tail -20"
ssh azureuser@${ORIGIN_IP} "sudo docker logs restaurant-1 2>&1 | tail -20"
ssh azureuser@${ORIGIN_IP} "sudo docker logs crapi-web 2>&1 | tail -20"

The upstream container is not ready or has crashed:

Terminal window
# Check nginx error log
ssh azureuser@${ORIGIN_IP} "sudo tail -20 /var/log/nginx/error.log"
# Restart all instances of a specific app
ssh azureuser@${ORIGIN_IP} "for i in 1 2 3 4; do sudo docker restart juice-shop-\$i; done"
# Rebuild and restart all containers
ssh azureuser@${ORIGIN_IP} "cd /opt/origin-server && sudo docker compose build && sudo docker compose up -d"

Docker images consume approximately 4.7 GiB. Access logging is disabled to prevent disk exhaustion under CDN load testing. Logrotate is configured as defense-in-depth (500 MiB cap). Verify disk usage:

Terminal window
ssh azureuser@${ORIGIN_IP} "df -h / && sudo docker system df"

Check for containers approaching their memory limits (OOM risk):

Terminal window
ssh azureuser@${ORIGIN_IP} "sudo docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' | sort"

If needed, rebuild and restart all services:

Terminal window
ssh azureuser@${ORIGIN_IP} "cd /opt/origin-server && sudo docker compose down && sudo docker compose build && sudo docker compose up -d && sudo systemctl restart nginx"
  • ECS does not stabilize: inspect aws ecs describe-services events. Verify private task-subnet DNS and HTTPS egress to Docker Hub and AWS Logs through caller-managed NAT, proxy, or equivalent endpoints.
  • Targets remain unhealthy: inspect describe-target-health, ECS events, and CloudWatch logs. Confirm the image listens on container_port, the target-group health check reaches /, and the task security group allows that port only from the ALB security group.
  • CloudWatch logs are absent: verify task egress or private endpoints, the regional Logs service principal permissions in the caller-managed KMS key policy, and the execution role’s log-stream permissions.
  • ALB logs are absent: verify the caller-managed bucket is in a supported region and its bucket policy permits regional Elastic Load Balancing log delivery to the configured prefix.
  • Internal URL is unreachable: verify DNS, routes, and source authorization from the approved connected network. Keep the ALB internal unless public exposure is a deliberate, reviewed requirement.
  • Terraform reports changes: review the plan against caller-managed VPC, routes, KMS, S3, provider authentication, and state. Do not apply until the ownership boundary and proposed changes are understood.