Blake Smith

create. code. learn.

»

Apex Domains for ActivityPub and Mastodon / GotoSocial with S3 and Cloudfront

If you are setting up an ActivityPub (Mastodon or GotoSocial) server, and want your domain to be at the “Apex” of your domain, and your domain is already hosting an existing website, you’ll need to do some additional setup. GotoSocial calls this a “Split Domain deployment”.

In my case, I wanted my ActivityPub user to be @blake@blakesmith.me, but my website is already hosted at blakesmith.me, meaning my ActivityPub server (GotoSocial in this case) needs a little extra configuration.

The setup is:

  • Host domain: social.blakesmith.me
  • Account domain: blakesmith.me

Here’s my NixOS module that configures GotoSocial (a minimal ActivityPub server, great for small and single user instances) with the split domain on my server. Notice the host and account-domain configuration:

{  pkgs, ... }:

{
  services.gotosocial = {
    enable = true;
    settings = {
      host = "social.blakesmith.me";
      account-domain = "blakesmith.me";
    };
  };

  services.nginx = {
    virtualHosts."social.blakesmith.me" = {
      forceSSL = true;
      enableACME = true;
      locations."/" = {
        proxyPass = "http://127.0.0.1:8080";
        extraConfig =
          "proxy_ssl_server_name on;" +
          "proxy_pass_header Authorization;" +
          "client_max_body_size 0;" +
          "proxy_http_version 1.1;" +
          "proxy_request_buffering off;" +
          "proxy_set_header Host $host;" +
          "proxy_set_header Upgrade $http_upgrade;" +
          "proxy_set_header Connection \"upgrade\";" +
          "proxy_set_header X-Forwarded-For $remote_addr;" +
          "proxy_set_header X-Forwarded-Proto $scheme;"
        ;
      };
    };
  };

  security.acme = {
    certs."social.blakesmith.me".email = "blakesmith0@gmail.com";
  };
}

We put nginx in front of GotoSocial, and setup a LetsEncrypt TLS cert as well (required for ActivityPub). This should all work great once DNS for social.blakesmith.me is pointed at the NixOS server. The GotoSocial daemon is reachable at social.blakesmith.me, but is configured to have usernames with blakesmith.me.

My website, blakesmith.me is hosted via an S3 bucket, with Cloudfront in front of it. We need to configure the S3 bucket for blakesmith.me to redirect all requests to the /.well-known route prefix to social.blakesmith.me. Here’s the terraform necessary:

resource "aws_s3_bucket" "origin_blakesmith_me" {
  bucket = "origin.blakesmith.me"
  # ...SNIP...
  website {
    index_document = "index.html"
    routing_rules = <<EOF
[{
    "Condition": {
        "KeyPrefixEquals": ".well-known"
    },
    "Redirect": {
        "HostName": "social.blakesmith.me",
        "HttpRedirectCode": "301",
        "Protocol": "https"
    }
}]
EOF
  }
}

The routing_rules is the most important part: Any route to the bucket that is prefixed with .well-known will be redirected to social.blakesmith.me. ActivityPub / Mastodon uses the WebFinger protocol as a way to resolve profile and server information for a given Mastodon handle.

Let’s test it:

$ curl -v "https://blakesmith.me/.well-known/webfinger?resource=acct:blake@blakesmith.me"
> GET /.well-known/webfinger?resource=acct:blake@blakesmith.me HTTP/2
> Host: blakesmith.me
> User-Agent: curl/8.4.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 301
< content-length: 0
< location: https://social.blakesmith.me/.well-known/webfinger?resource=acct:blake@blakesmith.me
< date: Sat, 17 Feb 2024 18:02:11 GMT
< server: AmazonS3
< x-cache: Hit from cloudfront
< via: 1.1 4076c139caa3b19374b9d2d1784ca5e0.cloudfront.net (CloudFront)
< x-amz-cf-pop: ORD56-P4
< x-amz-cf-id: XN5GXTM2fhACc4ZWmRbs2PMKmxEMeI-3yj0GZJ_orlLAp655feXVZQ==
< age: 3
<
* Connection #0 to host blakesmith.me left intact

It’s working if you get a valid redirect to your host domain server! If we follow redirects, we’ll hit the ActivityPub server hosted at social.blakesmith.me:

$ curl -L "https://blakesmith.me/.well-known/webfinger?resource=acct:blake@blakesmith.me" | jq .
{
  "subject": "acct:blake@blakesmith.me",
  "aliases": [
    "https://social.blakesmith.me/users/blake",
    "https://social.blakesmith.me/@blake"
  ],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://social.blakesmith.me/@blake"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://social.blakesmith.me/users/blake"
    }
  ]
}

If all is well, other users should be able to follow you using the account domain as your username (In my case: @blake@blakesmith.me).

One pitfall that tripped me up for awhile: if you have Cloudfront in front of your S3 bucket, like I do, you have to configure your Cloudfront origin using a custom origin and the S3 website endpoint, NOT the bucket endpoint. Otherwise, you’ll get “The specified key does not exist” errors when trying to access the webfinger resource at your account domain.

The relevant terraform setup for the Cloudfront distribution looks like this (notice how the bucket origin is configured with a custom_origin_config):

resource "aws_cloudfront_distribution" "blakesmith_distribution" {
  origin {
    domain_name = aws_s3_bucket.origin_blakesmith_me.website_endpoint
    origin_id = "blakesmith_origin"
    custom_origin_config {
      http_port = 80
      https_port = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols = [ "TLSv1.1", "TLSv1.2", "SSLv3" ]
    }
  }

  enabled = true
  is_ipv6_enabled = true
  default_root_object = "index.html"
  aliases = ["blakesmith.me", "www.blakesmith.me"]
  price_class = "PriceClass_200"
  retain_on_delete = true
  default_cache_behavior {
    allowed_methods = [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT" ]
    cached_methods = [ "GET", "HEAD" ]
    target_origin_id = "blakesmith_origin"
    forwarded_values {
      query_string = true
      cookies {
        forward = "none"
      }
    }
    viewer_protocol_policy = "redirect-to-https"
    min_ttl = 0
    default_ttl = 3600
    max_ttl = 86400
  }
  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate_validation.blakesmith_me.certificate_arn
    ssl_support_method = "sni-only"
  }
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
}

Feel free to follow me at @blake@blakesmith.me on ActivityPub!


about the author

Blake Smith is a Principal Software Engineer at Sprout Social.