# Protect an S3 website with qURL

> Protect an AWS website with a qURL Connector: connect your origin, choose who can enter, close public routes, and test access.

Source: https://layerv.ai/guides/protect-an-aws-website/

---

[← All guides](https://layerv.ai/guides/) 

# Protect an S3 website with qURL

Content updated November 6, 2026.

## How do I protect an S3 website from the command line?

Run the S3 static-origin adapter on your EC2 host, publish its loopback address with qURL™, and allow your company email domain. Then block public S3 access and close any old CloudFront route. You manage the setup from your terminal; no Slack app or LayerV operator is needed.

[Start the setup](https://layerv.ai/guides/protect-an-aws-website/#setup) 

## What you need

- An existing S3 static website in a standard AWS commercial region; the bucket name must not contain dots
- An EC2 Linux host with Docker, systemd, Python 3, and the AWS CLI; use a dedicated host for the site
- An IAM instance role on that host, and a separate AWS admin session that can add its bucket read policy
- The qURL CLI 3.7.1 or later
- For an existing public site: permission to close every public S3, CloudFront, and alternate origin route

## Steps

1. ### Set your site values  
Set these three values in your AWS admin terminal and in your EC2 shell. Run AWS commands from the admin terminal. Run Docker and qURL commands on EC2 as the same Linux user. Use the exact company email domain; subdomains are separate domains.  
```  
export AWS_REGION='us-east-1'
export S3_BUCKET='example-private-site'
export SITE_EMAIL_DOMAIN='example.com'
```
2. ### Give the host read access to this bucket  
Replace the role name with the IAM role already attached to your EC2 instance. This adds read access to this bucket only. The adapter uses temporary role credentials; do not put AWS access keys in the container. This example uses S3-managed encryption. A KMS-encrypted bucket also needs permission to decrypt with its key.  
```  
export SITE_ROLE='example-ec2-role'
cat > qurl-s3-read.json <<EOF
{"Version":"2012-10-17","Statement":[
  {"Effect":"Allow","Action":"s3:ListBucket","Resource":"arn:aws:s3:::$S3_BUCKET"},
  {"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::$S3_BUCKET/*"}
]}
EOF
aws iam put-role-policy --role-name "$SITE_ROLE" --policy-name "qurl-site-read-$S3_BUCKET" --policy-document file://qurl-s3-read.json
```
3. ### Start the private S3 origin  
The adapter listens on the host’s loopback address. Do not add a public port mapping. The :main tag tracks the latest tested image; --pull=always fetches it for this setup. Existing containers do not update automatically. Confirm that the local request returns your site before you continue.  
```  
docker run --pull=always -d --name qurl-s3-origin --restart unless-stopped --network host   -e AWS_REGION -e S3_BUCKET   ghcr.io/layervai/qurl-integrations/s3-static-connector:main
curl --fail http://127.0.0.1:8080/
```
4. ### Publish for your company  
qURL starts the background Connector, waits for it to serve, and prints the CRID entry link. Save that link. People must verify a mailbox in your exact company domain before access. Repeating publish must confirm the same policy; it does not silently change it. Keep this user’s qURL state when you update the host.  
```  
qurl publish http://127.0.0.1:8080 --id s3-website   --access email_domain --email-domain "$SITE_EMAIL_DOMAIN" -o json
```
5. ### Give the site a public title  
Replace the CRID below with the value printed by publish. Choose a short title that visitors can recognize. qURL shows it on the entry page and in access emails. Keep secrets and internal notes out of this public title.  
```  
export SITE_CRID='PASTE_CRID_HERE'
qurl preview set "$SITE_CRID" --name 'Example staging site'
```
6. ### Close public access  
First open the CRID entry link and check the site with an allowed email. Then enable all four S3 public-access blocks below. If CloudFront or another service can still read and serve the bucket, close that route too. Use the next step to disable a dedicated old CloudFront distribution. Do not disable a distribution shared with other applications. Check alternate domains and both A and AAAA DNS records. Publishing with qURL alone does not close an old route.  
```  
aws s3api put-public-access-block --bucket "$S3_BUCKET"   --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api get-public-access-block --bucket "$S3_BUCKET"
```
7. ### Close an old CloudFront distribution, if you have one  
Skip this step if the site never used CloudFront. Use only the distribution dedicated to this website. The command reads its current configuration, changes Enabled to false, and sends the matching version so a concurrent change cannot be overwritten. The configuration files stay in a private temporary directory. CloudFront can take several minutes to finish. Keep these changes in your infrastructure code.  
```  
export SITE_DISTRIBUTION_ID='EEXAMPLE123'
export QURL_CF_DIR="$(mktemp -d)"
aws cloudfront get-distribution-config --id "$SITE_DISTRIBUTION_ID" > "$QURL_CF_DIR/before.json"
python3 - <<'PYTHON'
import json, os
with open(os.path.join(os.environ['QURL_CF_DIR'], 'before.json')) as f:
    response = json.load(f)
response['DistributionConfig']['Enabled'] = False
request = {'Id': os.environ['SITE_DISTRIBUTION_ID'], 'IfMatch': response['ETag'],
           'DistributionConfig': response['DistributionConfig']}
with open(os.path.join(os.environ['QURL_CF_DIR'], 'disable.json'), 'w') as f:
    json.dump(request, f)
PYTHON
aws cloudfront update-distribution --cli-input-json "file://$QURL_CF_DIR/disable.json"
aws cloudfront wait distribution-deployed --id "$SITE_DISTRIBUTION_ID"
```
8. ### Check access, then share the entry link  
From a device with no active session, direct S3, old CloudFront, and old public website URLs must not return your site or its assets. Open the CRID link with an allowed email and check the pages, CSS, JavaScript, and images. An email outside the domain must be refused. Share the entry link only after these checks pass. App sign-in, callbacks, webhooks, and updaters need their own checks before you move an interactive site.
9. ### Keep it running  
Keep the EC2 host, Docker origin, qURL background service, and device state. On Linux, enable lingering for this user if the service must run after logout. Continue to upload site files to S3\. Keep the bucket and any old public routes private in your infrastructure code so later deployments do not reopen them.  
```  
sudo loginctl enable-linger "$USER"
qurl list -o json
```

## What the recipient sees

The visitor opens the CRID entry link, verifies their company email, and completes the browser access flow. An allowed visitor reaches the website. The CRID identifies the site; knowing it does not bypass the email policy.

## Troubleshooting

| If you see                                             | Do this                                                                                                                                                                                                                                                         |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The old website address still loads                    | Check A and AAAA records, alternate CloudFront hostnames, public bucket policies, and application load balancers. A Connector does not close an existing public route by itself.                                                                                |
| The original address returns “Not Found” before access | Use the CRID entry link printed by publish. The old public URL is not the new entry link. A denied S3 or CloudFront request is expected after origin lockdown. Keeping your own hostname as the private browser origin is a separate custom-domain setup.       |
| Sign-in or a machine client stops working              | Check callback origins, passkey relying-party settings, and whether the client can complete a CRID access flow. A browser session does not give a separate updater or webhook client access. Resolve that client’s access path before closing its public route. |

## Frequently Asked Questions

Can someone on the same network reach the site? 

Network admission can include devices that share the same public IP address. The private browser origin also requires a valid access grant for the visitor. Keep your application’s sign-in and permissions to control what each user can do.

Does this replace the application’s sign-in? 

No. The CRID controls who can reach the site. Your application still controls user accounts, roles, and actions.

Can I protect a site just by publishing its public URL? 

Publishing a URL does not close the original website. You must also restrict the origin and remove every public route to its content.

Can I do the whole setup with one CLI command? 

The publish command starts the Connector and sets the email policy. Docker serves the private S3 content. You must also close existing public AWS routes; publish cannot safely guess which CloudFront distributions or DNS records belong to this site.

## Related Resources

[Connector setup →](https://layerv.ai/start/#sidecar) [Enterprise Connector →](https://layerv.ai/platform/connector/) [Restrict an S3 origin with CloudFront ↗](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) [Route 53 alias records ↗](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values-alias-common.html)

## Publish your private site

After the local origin check passes, publish it with your company email domain.

[Or use the CLI ↗](https://layerv.ai/cli/)



---

*This markdown version is auto-generated from [https://layerv.ai/guides/protect-an-aws-website/](https://layerv.ai/guides/protect-an-aws-website/) for AI agents. Curated agent resources: [llms.txt](https://layerv.ai/llms.txt). For the full interactive experience, visit the HTML version.*

