On Bluesky and other ATProto-based platforms, users can associate their accounts with their own domain names instead of relying on service-owned subdomains. To do this, you need to prove that you control the domain by publishing a special file at a specific, “well-known” location or by adding a DNS record. Once verified, your account’s handle can become something like @username.example.com
instead of @username.bsky.social
.
For organizations that want to create multiple user accounts, using a separate hostname for each user makes management easier. By setting up a wildcard DNS record (e.g., *.example.com
), you can quickly create new hostnames for any user without having to run separate websites or add individual DNS entries. Each of these hostnames can return a unique DID (Decentralized Identifier) to ATProto services through a simple, automated process.
In the following example, DID values are stored as simple JSON files. A small CGI script reads the hostname requested, finds the matching JSON file, and serves it at the well-known route. This JSON file not only provides the DID needed for verification on Bluesky or other ATProto services, but also includes additional information like a user’s name and a redirect URL. When someone visits the root of the user’s hostname, they’re automatically redirected to a profile page—such as a LinkedIn profile or an organizational directory entry—providing a unified way to confirm the user’s identity and affiliation.
By setting up a system like this, organizations can maintain complete control over their identities on social media platforms, guide visitors back to their own web properties, and ensure that their domain names remain the authoritative source for verifying user identities.
- Domain Handle Tutorial (Bluesky)
-
Launch an EC2 Instance:
- Go to the AWS Management Console.
- Navigate to EC2 and click Launch Instances.
- Select an Amazon Linux 2023 AMI.
- Choose an ARM-based instance type, such as
t4g.micro
, for cost efficiency.
-
Configure Instance Details:
- Choose default settings for network.
- Ensure SSH access is enabled.
-
Add Storage:
- Use the default storage configuration.
-
Launch and Connect:
-
Download the key pair (
.pem
file). -
Connect using SSH:
ssh -i /path/to/key.pem ec2-user@<your-instance-public-ip>
-
-
Update Packages:
sudo yum update -y
-
Install Apache:
sudo yum install -y httpd sudo systemctl start httpd sudo systemctl enable httpd
-
Open Route 53:
- Navigate to Route 53 in the AWS Console.
- Select your hosted zone for the domain (e.g.,
example.com
).
-
Add a Wildcard DNS Record:
- Create an A Record:
- Name:
*.example.com
- Type: A
- Value: Public IP of your EC2 instance.
- Name:
- Add an additional A Record for
example.com
pointing to the same IP.
- Create an A Record:
-
Test the DNS Record:
dig user1.example.com
-
Create the VirtualHost Configuration: Edit the Apache configuration file:
sudo vim /etc/httpd/conf.d/example.com.conf
Add the following:
<VirtualHost *:80> ServerName example.com ServerAlias *.example.com DocumentRoot "/var/www/html" ScriptAlias /cgi-bin/ /var/www/cgi-bin/ Alias /.well-known/atproto-did /var/www/cgi-bin/atproto-did.cgi <Directory "/var/www/cgi-bin"> AllowOverride None Options +ExecCGI Require all granted </Directory> </VirtualHost>
-
Restart Apache:
sudo systemctl restart httpd
-
Create the CGI Script:
sudo vim /var/www/cgi-bin/atproto-did.cgi
Paste the script:
#!/bin/bash # Base directory for profile JSON files PROFILE_DIR="/var/www/profiles" # Get the hostname (subdomain part) HOSTNAME=$(echo "$HTTP_HOST" | cut -d. -f1) # Validate that HOSTNAME only contains valid characters: letters, numbers, hyphens, and underscores if [[ ! "$HOSTNAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then echo "Content-Type: text/plain" echo echo "Invalid hostname." exit 1 fi # Path to the profile JSON file PROFILE_FILE="${PROFILE_DIR}/${HOSTNAME}.json" # Serve /.well-known/atproto-did if [[ "$REQUEST_URI" == "/.well-known/atproto-did" ]]; then if [[ -f "$PROFILE_FILE" ]]; then # Extract the "did" value using jq DID_PLC=$(jq -r ".\"did\"" "$PROFILE_FILE") # Ensure both values exist before responding if [[ -n "$DID_PLC" ]]; then echo "Content-Type: text/plain" echo echo "$DID_PLC" else # Missing required fields echo "Content-Type: text/plain" echo echo "Missing required DID fields in the profile." fi else # Profile file not found echo "Content-Type: text/plain" echo echo "Profile not found." fi exit 0 fi # Redirect to the profile URL for root access if [[ -f "$PROFILE_FILE" ]]; then REDIRECT_URL=$(jq -r ".url" "$PROFILE_FILE") if [[ -n "$REDIRECT_URL" ]]; then echo "Content-Type: text/html" echo "Status: 302 Found" echo "Location: $REDIRECT_URL" echo echo "<html><head><title>Redirecting...</title></head><body>Redirecting to <a href=\"$REDIRECT_URL\">$REDIRECT_URL</a></body></html>" else echo "Content-Type: text/plain" echo echo "Redirect URL not found." fi else echo "Content-Type: text/plain" echo echo "Profile not found." fi
-
Set Permissions:
sudo chmod +x /var/www/cgi-bin/atproto-did.cgi
-
Create the Profiles Directory:
sudo mkdir -p /var/www/profiles sudo vim /var/www/profiles/user1.json
Example JSON:
{ "name": "User One", "url": "https://www.linkedin.com/in/user1", "did-plc": "did:plc:z72i7hdynmk6r22h6tvur", }
-
Create IAM User:
-
Go to IAM → Users → Add Users.
-
Assign programmatic access and attach the following policy:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "route53:ListHostedZones", "route53:GetChange", "route53:ChangeResourceRecordSets" ], "Resource": "*" } ] }
-
-
Store AWS Credentials:
mkdir -p ~/.aws vim ~/.aws/credentials
Add:
[default] aws_access_key_id = YOUR_ACCESS_KEY aws_secret_access_key = YOUR_SECRET_KEY
-
Copy Credentials for
root
:sudo mkdir -p /root/.aws sudo cp ~/.aws/credentials /root/.aws/credentials sudo chmod 600 /root/.aws/credentials
-
Install Certbot and Route 53 Plugin:
sudo yum install -y python3-certbot-dns-route53
-
Run Certbot:
sudo certbot -a dns-route53 -i apache -d "*.example.com" -d "example.com"
-
Verify SSL: Test HTTPS for both
https://example.com
andhttps://sub.example.com
.
-
Create a Systemd Timer:
sudo vim /etc/systemd/system/certbot-renew.service
Add:
[Unit] Description=Certbot Renewal [Service] Type=oneshot ExecStart=/usr/bin/certbot renew --quiet
-
Create the Timer:
sudo vim /etc/systemd/system/certbot-renew.timer
Add:
[Unit] Description=Run Certbot Renewal Twice Daily [Timer] OnCalendar=*-*-* 00,12:00:00 Persistent=true [Install] WantedBy=timers.target
-
Enable the Timer:
sudo systemctl enable certbot-renew.timer sudo systemctl start certbot-renew.timer
-
Test the Timer:
sudo certbot renew --dry-run
Update the VirtualHost under /etc/httpd/conf.d
which should now have rewrite rules. Add the following rule to redirect HTTP requests to HTTPS.
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]