Skip to content

Configuration

Let’s assume that cattle grid is running on its own domain cattlegrid.yourdomain.example. Then you can give the cattle grid actor the id https://cattlegrid.yourdomain.example/actor.

Info

For how to use cattle_grid together with an application, see the corresponding bovine_herd tutorial.

Setting up cattle grid

First cattle_grid can be installed from PyPI via

pip install cattle-grid

Then one can create the configuration file (including generating public and private keys) using

python -mcattle_grid.config

where you have to enter an actor id, We assume for this that you use https://cattlegrid.yourdomain.example/actor. The details for this command are

python -m cattle_grid.config

Usage:

python -m cattle_grid.config [OPTIONS]

Options:

  --actor_id TEXT  Actor id with schema, e.g.
                   http://cattle_grid/cattle_grid_actor
  --username TEXT  Used to in acct:username@domain. domain taken from actor
                   id, leave blank for random
  --db_url TEXT    database url by default sqlite file cattle_grid.sqlite
  --recreate       Allows you to overwrite an existing cattle_grid.toml file
  --help           Show this message and exit.

The configuration is stored in the cattle_grid.toml. The details of the config object are available here.

We furthermore, recommend that you set up a blocklist using for example Seirdy’s FediNuke by running

python -mcattle_grid.block

You can now run cattle_grid via

uvicorn --factory cattle_grid:create_app --uds /tmp/cattle_grid.sock

systemd unit

To run cattle_grid as a systemd service, the unit file would look like

/etc/systemd/system/cattle_grid.service
[Unit]
Description=cattle grid
After=network.target

[Service]
User=cattle_grid
Group=cattle_grid
Restart=always
Type=simple
WorkingDirectory=/opt/cattle_grid
ExecStart=uvicorn --factory cattle_grid:create_app --uds /tmp/cattle_grid.sock

[Install]
WantedBy=multi-user.target

nginx configuration for cattle_grid’s server

If you are running cattle_grid on the domain mentioned above, the nginx configuration would look like:

server {
    listen 80;
    server_name cattlegrid.yourdomain.example;

    location /auth {
        return 401;
    }

    location / {
        proxy_pass http://unix:/tmp/cattle_grid.sock;
    }
}

The above snippet skips details such as configuring SSL. We do not need to add any additional headers to the requests to / as cattle_grid does not check signatures for requests to its actor. See here for a sequence diagram why this is necessary.

nginx configuration for your application

For details of what this configuration does, see request flow. The simplest example of a configuration is

/etc/nginx/conf.d/your_application.conf
server {
    listen 80 default_server;

    location / {
        auth_request /auth;
        auth_request_set $requester $upstream_http_x_cattle_grid_requester;

        proxy_pass http://your_application;
        proxy_set_header X-Cattle-Grid-Requester $requester;
    }

    location = /auth {
        internal;
        proxy_pass http://unix:/tmp/cattle_grid.sock;
        proxy_pass_request_body off;
        proxy_set_header X-Original-URI $request_uri;
        proxy_set_header X-Original-Method $request_method;
        proxy_set_header X-Original-Host $host;
        proxy_set_header X-Original-Port $server_port;
    }
}

This will lead to all correctly signed requests having the X-Cattle-Grid-Requester header containing the requester. If there is no signature, this header is empty. If the request is rejected either due to having an invalid signature (401) or being blocked (403), your application does not see the request.

Config dataclass

Dataclass holding the configuration of cattle_grid

Parameters:

Name Type Description Default
actor_id str

The URL the actor will be available at

required
actor_acct_id str

The acct id to use (reserves the username)

required
db_url str

url of the database used to store remote identities

required
public_key str

public key of cattle_grid actor

required
private_key str

private key of cattle_grid actor

required
Source code in cattle_grid/config.py
@dataclass
class Config:
    """Dataclass holding the configuration of cattle_grid

    :param actor_id: The URL the actor will be available at
    :param actor_acct_id: The acct id to use (reserves the username)
    :param db_url: url of the database used to store remote identities

    :param public_key: public key of cattle_grid actor
    :param private_key: private key of cattle_grid actor"""

    actor_id: str
    actor_acct_id: str
    db_url: str

    public_key: str
    private_key: str

    domain_blocks: Set[str] = field(default_factory=set)

    def is_blocked(self, uri: str) -> bool:
        """Checks if domain is blocked.

        :param uri: URI to check if its domain is blocked
        :return: `True` if blocked, `False` otherwise."""
        try:
            domain = urlparse(uri).netloc
            return domain in self.domain_blocks
        except Exception as e:
            logger.warning("Something went wrong with %s", repr(e))
            return True

    @staticmethod
    def new(
        actor_id,
        actor_acct_id,
        db_url="sqlite://cattle_grid.sqlite",
        domain_blocks: Set[str] = set(),
    ):
        """Creates a new Config object.

        :param actor_id: The actor id, contains the domain name
        :param actor_acct_id: The actor account id of the from `acct:username@domain`
        :param db_url: the database connection
        :param domain_blocks: domain blocks.
        """
        public_key, private_key = generate_rsa_public_private_key()

        return Config(
            actor_id=actor_id,
            actor_acct_id=actor_acct_id,
            db_url=db_url,
            public_key=public_key,
            private_key=private_key,
            domain_blocks=domain_blocks,
        )

    @staticmethod
    def load(filename="cattle_grid.toml"):
        """Loads the configuration

        :param filename: file to load"""
        with open(filename, "rb") as fp:
            result = Config(**tomllib.load(fp))
            result.domain_blocks = set(result.domain_blocks)
            return result

    def save(self, filename="cattle_grid.toml"):
        """Saves the configuration

        :param filename: file to write to"""
        with open(filename, "wb") as fp:
            data = asdict(self)
            data["domain_blocks"] = sorted(list(self.domain_blocks))
            tomli_w.dump(data, fp, multiline_strings=True)

    @property
    def actor_path(self):
        return urlparse(self.actor_id).path

    @cached_property
    def actor(self):
        username, _ = parse_fediverse_handle(self.actor_acct_id.removeprefix("acct:"))
        return Actor(
            id=self.actor_id,
            type="Service",
            public_key=self.public_key,
            preferred_username=username,
            public_key_name="mykey",
        ).build()

    @cached_property
    def webfinger(self):
        return webfinger_response_json(self.actor_acct_id, self.actor_id)

    @property
    def bovine_actor(self):
        actor = BovineActor(
            actor_id=self.actor_id,
            secret=self.private_key,
            public_key_url=self.actor_id + "#mykey",
        )
        return actor

    def add_blocks_from_url_or_file(self, url_or_file: str):
        """Adds the list of domains given by `url_or_file` to the
        blocklist. Assumes that each domain is on a new line.

        :param url_or_file: If it starts with `https://` is assumed to be an url
            otherwise as a file.
        """
        if url_or_file.startswith("https://"):
            with urlopen(url_or_file) as f:
                data = f.readlines()
        else:
            with open(url_or_file) as f:
                data = f.readlines()

        data = {
            x.decode("utf-8").removesuffix("\n")
            for x in data
            if x != b"canary.fedinuke.example.com\n"
        }

        self.domain_blocks = self.domain_blocks | data

add_blocks_from_url_or_file(url_or_file)

Adds the list of domains given by url_or_file to the blocklist. Assumes that each domain is on a new line.

Parameters:

Name Type Description Default
url_or_file str

If it starts with https:// is assumed to be an url otherwise as a file.

required
Source code in cattle_grid/config.py
def add_blocks_from_url_or_file(self, url_or_file: str):
    """Adds the list of domains given by `url_or_file` to the
    blocklist. Assumes that each domain is on a new line.

    :param url_or_file: If it starts with `https://` is assumed to be an url
        otherwise as a file.
    """
    if url_or_file.startswith("https://"):
        with urlopen(url_or_file) as f:
            data = f.readlines()
    else:
        with open(url_or_file) as f:
            data = f.readlines()

    data = {
        x.decode("utf-8").removesuffix("\n")
        for x in data
        if x != b"canary.fedinuke.example.com\n"
    }

    self.domain_blocks = self.domain_blocks | data

is_blocked(uri)

Checks if domain is blocked.

Parameters:

Name Type Description Default
uri str

URI to check if its domain is blocked

required

Returns:

Type Description
bool

True if blocked, False otherwise.

Source code in cattle_grid/config.py
def is_blocked(self, uri: str) -> bool:
    """Checks if domain is blocked.

    :param uri: URI to check if its domain is blocked
    :return: `True` if blocked, `False` otherwise."""
    try:
        domain = urlparse(uri).netloc
        return domain in self.domain_blocks
    except Exception as e:
        logger.warning("Something went wrong with %s", repr(e))
        return True

load(filename='cattle_grid.toml') staticmethod

Loads the configuration

Parameters:

Name Type Description Default
filename

file to load

'cattle_grid.toml'
Source code in cattle_grid/config.py
@staticmethod
def load(filename="cattle_grid.toml"):
    """Loads the configuration

    :param filename: file to load"""
    with open(filename, "rb") as fp:
        result = Config(**tomllib.load(fp))
        result.domain_blocks = set(result.domain_blocks)
        return result

new(actor_id, actor_acct_id, db_url='sqlite://cattle_grid.sqlite', domain_blocks=set()) staticmethod

Creates a new Config object.

Parameters:

Name Type Description Default
actor_id

The actor id, contains the domain name

required
actor_acct_id

The actor account id of the from acct:username@domain

required
db_url

the database connection

'sqlite://cattle_grid.sqlite'
domain_blocks Set[str]

domain blocks.

set()
Source code in cattle_grid/config.py
@staticmethod
def new(
    actor_id,
    actor_acct_id,
    db_url="sqlite://cattle_grid.sqlite",
    domain_blocks: Set[str] = set(),
):
    """Creates a new Config object.

    :param actor_id: The actor id, contains the domain name
    :param actor_acct_id: The actor account id of the from `acct:username@domain`
    :param db_url: the database connection
    :param domain_blocks: domain blocks.
    """
    public_key, private_key = generate_rsa_public_private_key()

    return Config(
        actor_id=actor_id,
        actor_acct_id=actor_acct_id,
        db_url=db_url,
        public_key=public_key,
        private_key=private_key,
        domain_blocks=domain_blocks,
    )

save(filename='cattle_grid.toml')

Saves the configuration

Parameters:

Name Type Description Default
filename

file to write to

'cattle_grid.toml'
Source code in cattle_grid/config.py
def save(self, filename="cattle_grid.toml"):
    """Saves the configuration

    :param filename: file to write to"""
    with open(filename, "wb") as fp:
        data = asdict(self)
        data["domain_blocks"] = sorted(list(self.domain_blocks))
        tomli_w.dump(data, fp, multiline_strings=True)