From 1be8f9ad68c70d2efef33598ac877162c650674e Mon Sep 17 00:00:00 2001 From: Jasper Mayone Date: Mon, 28 Sep 2026 16:47:39 -0400 Subject: [PATCH 1/2] feat: check zone files for owners, ttl and cloudflare rules --- .github/PULL_REQUEST_TEMPLATE.md | 3 +- CLAUDE.md | 13 +- CONTRIBUTING.md | 5 +- README.md | 23 +++- bin/validate | 8 +- docs/runbook.md | 30 ++++- tools/check_zones.py | 188 ++++++++++++++++++++++++++ tools/test_check_zones.py | 225 +++++++++++++++++++++++++++++++ 8 files changed, 478 insertions(+), 17 deletions(-) create mode 100644 tools/check_zones.py create mode 100644 tools/test_check_zones.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ad71c8e..433b744 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,7 +17,8 @@ know who to ask when it breaks. For example: ## Checklist - [ ] The record is for a Patchwork project, event, or service. -- [ ] Every record I added or changed has an owner in a comment. +- [ ] Every record I added or changed has an owner in a comment: an email or a + GitHub handle. - [ ] `./bin/validate` passes, so the records are in the order octoDNS wants. - [ ] CNAME values end with a dot. A and AAAA values do not. - [ ] I have read the `octoDNS plan` comment on this pull request and it diff --git a/CLAUDE.md b/CLAUDE.md index 8ba0780..d404cc5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,9 +13,9 @@ The repository uses OctoDNS with Cloudflare as the DNS provider and YAML configu ## Architecture - **Configuration**: `config/config.yaml` defines providers, processors and zone mappings. `enforce_order` with `order_mode: natural` is on. -- **DNS Records**: Domain-specific YAML files (`patchworklabs.org.yaml`, `hackathon.help.yaml`) contain DNS record definitions. Every new record needs an owner email in a comment on the same line as its name. +- **DNS Records**: Domain-specific YAML files (`patchworklabs.org.yaml`, `hackathon.help.yaml`) contain DNS record definitions. Every record needs an owner (an email or a GitHub handle) in a comment on the same line as its name. - **Scripts**: Shell scripts in `bin/` handle DNS operations. They read the zone list from `config/config.yaml` through `bin/zones`. -- **Tools**: `tools/merge_live.py` merges live Cloudflare state back into the zone files and keeps comments. Tests are in `tools/test_merge_live.py`. +- **Tools**: `tools/merge_live.py` merges live Cloudflare state back into the zone files and keeps comments. `tools/check_zones.py` checks the repository rules (owner comment on every record, TTL of 120 or more, no apex NS, no `octodns-meta`, proxy only on A/AAAA/CNAME). `./bin/validate` runs it. Tests are in `tools/test_*.py`. - **Workflows**: `validate` (no secrets), `plan` (`pull_request_target`, posts the plan and checks drift), `deploy` (push to `main`), `sync-from-cloudflare` (nightly). - **Dependencies**: Python dependencies pinned in `requirements.txt`. @@ -78,11 +78,12 @@ Use appropriate TTL values based on record type and change frequency: ### Record Comments Format ```yaml # Google Workspace email routing - DO NOT MODIFY without IT approval -mx: - values: - - exchange: mx1.example.com - priority: 10 +"": # @patchworklabsorg/infra ttl: 3600 + type: MX + values: + - exchange: mx1.example.com. + preference: 10 ``` ## Important Notes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 554751b..cf16d2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,8 @@ Thank you for helping run the Patchwork Labs DNS. This file covers the rules. Th 3. Keep records in order inside a zone file. The `dns records` check enforces it. The order is natural, so `ns2` comes before `ns10`, and inside a record `octodns` comes before `ttl`, `type` and `value`. Run `./bin/validate`. -4. Give every record an owner in a comment on the same line as its name. +4. Give every record an owner in a comment on the same line as its name. Use + an email or a GitHub handle. The `dns records` check enforces it. 5. Read the `octoDNS plan` comment on your pull request before you ask for a review. It is the exact list of changes the merge will make. 6. Answer review comments on the same pull request. Do not close it and open a @@ -82,7 +83,7 @@ pull request the next morning. $ python -m unittest discover -s tools -p 'test_*.py' -v ``` -Add a test with any change to `tools/merge_live.py`. That script edits the zone +Add a test with any change to `tools/merge_live.py` or `tools/check_zones.py`. That script edits the zone files by itself every night, so a bug in it is a bug in production DNS. Never loosen the security note at the top of diff --git a/README.md b/README.md index d67f3c2..14c8c69 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ Three rules decide whether it works: - **The name is the part before the domain.** `docs` becomes `docs.patchworklabs.org`. - **A `CNAME` value ends with a dot.** An `A` or `AAAA` value does not. -- **Every record needs an owner.** Put a Patchwork email in a comment on the same - line as the name. We use it to find out who to ask when the record breaks. - List more than one person if more than one person is responsible. +- **Every record needs an owner.** Put an email or a GitHub handle in a + comment on the same line as the name, for example `# ada@patchworklabs.org` + or `# @patchworklabsorg/infra`. We use it to find out who to ask when the + record breaks. List more than one owner if more than one person is + responsible. The `dns records` check fails on a record without an owner. ### Order is checked @@ -54,6 +56,21 @@ a rule and not a request. The order is **natural**, not plain alphabetical: Run `./bin/validate` to check before you push. The nightly sync writes files in this order by itself. +### Other checks + +`./bin/validate` also runs [`tools/check_zones.py`](./tools/check_zones.py). +It fails when: + +- A record has no owner comment, or only a `TODO owner unknown` comment. +- A TTL is lower than 120 seconds. Cloudflare raises a lower TTL to 120, so + the zone file would never match Cloudflare. +- A zone file holds the apex `NS` records. Cloudflare owns them. +- A zone file holds the `octodns-meta` record. octoDNS writes it. +- A record that is not `A`, `AAAA` or `CNAME` is behind the Cloudflare proxy. +- A zone in `config/config.yaml` has no zone file, or a YAML file at the + repository root is not a zone. +- One name appears twice in one file. + ### 2. Open a pull request A bot posts a **plan** on your pull request. It lists every record the merge diff --git a/bin/validate b/bin/validate index 09aa2a4..f6ce168 100755 --- a/bin/validate +++ b/bin/validate @@ -1,9 +1,13 @@ #!/bin/sh # Check that the config and the zone files parse and that every record is -# valid. Does not contact Cloudflare, so it needs no real token. +# valid. Then check the repository rules that octoDNS does not know about, +# such as the owner comment on every record. See tools/check_zones.py. +# +# Does not contact Cloudflare, so it needs no real token. set -eu CLOUDFLARE_TOKEN="${CLOUDFLARE_TOKEN:-validate-only-not-a-real-token}" export CLOUDFLARE_TOKEN -exec octodns-validate --config-file=./config/config.yaml "$@" +octodns-validate --config-file=./config/config.yaml "$@" +python3 tools/check_zones.py diff --git a/docs/runbook.md b/docs/runbook.md index 44e24b7..0b29389 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -28,9 +28,33 @@ There is also one repository **variable**, not a secret: |---|---| | `DNS_BOT_CLIENT_ID` | Client ID of the same App. An identifier, not a credential | -The two Cloudflare tokens already exist. Create them at -**Cloudflare > My Profile > API Tokens** with the `Edit zone DNS` template, and -scope each one to `patchworklabs.org` and `hackathon.help` only. +The two zones are in **different Cloudflare accounts**: + +| Zone | Cloudflare account | +|---|---| +| `patchworklabs.org` | Patchwork Labs | +| `hackathon.help` | Jasper Mayone | + +An account API token can only see the zones in its own account. Use a **user** +token, which can cover zones in every account its owner belongs to. Create +each one at **Cloudflare > My Profile > API Tokens**: + +1. Select **Create Custom Token**. +2. **Permissions**: `Zone` `Zone` `Read`, and `Zone` `DNS` `Edit` for + `CLOUDFLARE_TOKEN` or `Zone` `DNS` `Read` for + `CLOUDFLARE_TOKEN_READ_ONLY`. +3. **Zone Resources**: `Include` `Specific zone` `patchworklabs.org`, then add + a second row for `hackathon.help`. +4. Save the value straight into the repository secret. Do not paste it + anywhere else: + + ```console + $ gh secret set CLOUDFLARE_TOKEN --repo patchworklabsorg/dns + ``` + +A token that cannot see a zone makes octoDNS try to create that zone. The +deploy then fails with `Invalid account identifier passed in your organization +variable`. > **Rotate `CLOUDFLARE_TOKEN_READ_ONLY` once.** The old `test.yml` workflow > ran scripts from a pull request while holding it, so anybody who opened a diff --git a/tools/check_zones.py b/tools/check_zones.py new file mode 100644 index 0000000..2aaaf4e --- /dev/null +++ b/tools/check_zones.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Check the zone files for the rules that octoDNS does not know about. + +``octodns-validate`` checks that each record is valid DNS. It does not know the +rules of this repository, so this script checks them: + +1. Every record has an owner in a comment on the same line as its name. An + owner is an email address or a GitHub handle, for example + ``# ada@patchworklabs.org`` or ``# @patchworklabsorg/infra``. A + ``TODO owner unknown`` comment from the nightly sync is not an owner. +2. Every TTL is at least the Cloudflare minimum of 120 seconds. A lower value + is silently raised by Cloudflare, so the zone file would never match it. +3. No zone file holds the apex NS records. Cloudflare owns them. +4. No zone file holds the ``octodns-meta`` record. octoDNS writes it. +5. Only A, AAAA and CNAME records are behind the Cloudflare proxy. +6. Every zone in ``config/config.yaml`` has a zone file, and every YAML file + at the repository root belongs to a zone. +7. No record name appears twice in one file. + +Usage: + + python3 tools/check_zones.py [--repo-dir .] + +Exits 0 when every file passes and 1 when any rule fails. Each failure is +printed as ``file:line: message``. +""" + +import argparse +import re +import sys +from pathlib import Path + +import yaml + +MIN_TTL = 120 +PROXIABLE = {"A", "AAAA", "CNAME"} +META_RECORD = "octodns-meta" + +# A top level key: `name:`, `"name":` or `'name':`, then an optional comment. +_TOP_LEVEL = re.compile( + r"""^(?:"(?P[^"]*)"|'(?P[^']*)'|(?P[^\s#"'-][^:#]*?))\s*:""" + r"""(?:\s+(?P.*))?$""" +) +_EMAIL = re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+") +_HANDLE = re.compile(r"(?:^|[\s,])@[A-Za-z0-9](?:[A-Za-z0-9-]*)(?:/[\w.-]+)?") + + +def _owner_comment(rest): + """Return the comment on a key line, or None.""" + if not rest: + return None + if rest.startswith("#"): + return rest[1:].strip() + match = re.search(r"\s#(.*)$", rest) + return match.group(1).strip() if match else None + + +def has_owner(comment): + """True when the comment names at least one owner.""" + if not comment or comment.lower().startswith("todo"): + return False + return bool(_EMAIL.search(comment) or _HANDLE.search(comment)) + + +def _top_level_keys(text): + """Yield (line number, name, comment) for each record in a zone file.""" + for number, line in enumerate(text.split("\n"), start=1): + if line == "---": + continue + match = _TOP_LEVEL.match(line) + if not match: + continue + name = next( + g for g in (match.group("dq"), match.group("sq"), match.group("bare")) + if g is not None + ) + yield number, name, _owner_comment(match.group("rest")) + + +def _records(value): + """A record name holds one record or a list of them.""" + if isinstance(value, list): + return value + return [value] + + +def check_file(path): + """Return a list of `file:line: message` strings for one zone file.""" + text = path.read_text() + errors = [] + + lines = {} + for number, name, comment in _top_level_keys(text): + if name in lines: + errors.append( + f"{path.name}:{number}: `{name or '@'}` is already defined on " + f"line {lines[name]}" + ) + continue + lines[name] = number + if not has_owner(comment): + errors.append( + f"{path.name}:{number}: `{name or '@'}` has no owner. Add an " + f"email or a GitHub handle in a comment on the same line" + ) + + data = yaml.safe_load(text) or {} + if not isinstance(data, dict): + return errors + [f"{path.name}:1: the file is not a mapping of records"] + + for name, value in data.items(): + number = lines.get(name, 1) + label = name or "@" + if name == META_RECORD: + errors.append( + f"{path.name}:{number}: `{META_RECORD}` is written by octoDNS. " + f"Remove it from the zone file" + ) + for record in _records(value): + if not isinstance(record, dict): + continue + rtype = str(record.get("type", "")).upper() + ttl = record.get("ttl") + if ttl is not None and ttl < MIN_TTL: + errors.append( + f"{path.name}:{number}: `{label}` {rtype} has ttl {ttl}. " + f"The Cloudflare minimum is {MIN_TTL}" + ) + if name == "" and rtype == "NS": + errors.append( + f"{path.name}:{number}: the apex NS records belong to " + f"Cloudflare. Remove them from the zone file" + ) + octodns = record.get("octodns") or {} + proxied = (octodns.get("cloudflare") or {}).get("proxied") + if proxied and rtype not in PROXIABLE: + errors.append( + f"{path.name}:{number}: `{label}` {rtype} cannot be " + f"proxied. Only {', '.join(sorted(PROXIABLE))} can" + ) + return errors + + +def _config_zones(repo_dir): + with open(Path(repo_dir) / "config" / "config.yaml") as fh: + return [z.rstrip(".") for z in yaml.safe_load(fh)["zones"]] + + +def check_repo(repo_dir): + """Return every error for the repository.""" + repo = Path(repo_dir) + zones = _config_zones(repo) + errors = [] + + files = {p.name for p in repo.glob("*.yaml")} | { + p.name for p in repo.glob("*.yml") + } + expected = {f"{zone}.yaml" for zone in zones} + for name in sorted(expected - files): + errors.append(f"{name}:1: the zone is in config/config.yaml but the file is missing") + for name in sorted(files - expected): + errors.append( + f"{name}:1: this file is not a zone in config/config.yaml, so " + f"octoDNS would ignore it" + ) + + for name in sorted(expected & files): + errors.extend(check_file(repo / name)) + return errors + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--repo-dir", default=".", help="where the zone files live") + args = parser.parse_args(argv) + + errors = check_repo(args.repo_dir) + for error in errors: + print(error) + if errors: + print(f"\n{len(errors)} problem(s). See README.md for the rules.") + return 1 + print("Every zone file follows the repository rules.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/test_check_zones.py b/tools/test_check_zones.py new file mode 100644 index 0000000..1bde65b --- /dev/null +++ b/tools/test_check_zones.py @@ -0,0 +1,225 @@ +"""Tests for tools/check_zones.py. + +Run them with: + + python3 -m unittest discover -s tools -p 'test_*.py' -v +""" + +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from check_zones import check_file, check_repo, has_owner # noqa: E402 +from merge_live import merge_zone # noqa: E402 + +CONFIG = """--- +zones: + patchworklabs.org.: + sources: [config] + targets: [cloudflare] +""" + +GOOD = """--- +"": # @patchworklabsorg/infra + - ttl: 120 + type: MX + values: + - exchange: aspmx.l.google.com. + preference: 1 + +# A comment above a record is fine as well. +api: # ada@patchworklabs.org, @grace + - octodns: + cloudflare: + proxied: true + ttl: 300 + type: CNAME + value: api.example.com. + +docs: # grace@patchworklabs.org + type: TXT + value: no-ttl-uses-the-default +""" + + +class HasOwnerTest(unittest.TestCase): + def test_email(self): + self.assertTrue(has_owner("ada@patchworklabs.org")) + + def test_github_user(self): + self.assertTrue(has_owner("@jaspermayone")) + + def test_github_team(self): + self.assertTrue(has_owner("@patchworklabsorg/infra")) + + def test_several(self): + self.assertTrue(has_owner("ada@patchworklabs.org, @grace")) + + def test_none(self): + self.assertFalse(has_owner(None)) + self.assertFalse(has_owner("")) + + def test_free_text(self): + self.assertFalse(has_owner("Google Workspace DKIM")) + + def test_todo_from_the_nightly_sync(self): + self.assertFalse( + has_owner("TODO owner unknown, added from Cloudflare on 2026-09-21") + ) + + +class CheckFileTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.path = Path(self.tmp.name) / "patchworklabs.org.yaml" + + def check(self, body): + self.path.write_text(body) + return check_file(self.path) + + def assertOneError(self, body, fragment): + errors = self.check(body) + self.assertEqual(len(errors), 1, errors) + self.assertIn(fragment, errors[0]) + + def test_good_file_passes(self): + self.assertEqual(self.check(GOOD), []) + + def test_missing_owner(self): + self.assertOneError( + "---\napi:\n ttl: 300\n type: A\n value: 192.0.2.1\n", + "patchworklabs.org.yaml:2: `api` has no owner", + ) + + def test_owner_on_the_line_above_does_not_count(self): + self.assertOneError( + "---\n# ada@patchworklabs.org\napi:\n ttl: 300\n type: A\n" + " value: 192.0.2.1\n", + "`api` has no owner", + ) + + def test_apex_without_owner_is_named_at(self): + self.assertOneError( + '---\n"":\n ttl: 300\n type: A\n value: 192.0.2.1\n', + "`@` has no owner", + ) + + def test_single_quoted_apex(self): + body = "---\n'': # @jaspermayone\n ttl: 300\n type: A\n value: 192.0.2.1\n" + self.assertEqual(self.check(body), []) + + def test_todo_owner_fails(self): + self.assertOneError( + "---\napi: # TODO owner unknown, added from Cloudflare on 2026-09-21\n" + " ttl: 300\n type: A\n value: 192.0.2.1\n", + "has no owner", + ) + + def test_low_ttl(self): + self.assertOneError( + "---\napi: # @ada\n ttl: 1\n type: A\n value: 192.0.2.1\n", + "`api` A has ttl 1. The Cloudflare minimum is 120", + ) + + def test_low_ttl_inside_a_list(self): + self.assertOneError( + "---\napi: # @ada\n - ttl: 300\n type: A\n value: 192.0.2.1\n" + " - ttl: 60\n type: TXT\n value: hello\n", + "`api` TXT has ttl 60", + ) + + def test_apex_ns(self): + self.assertOneError( + '---\n"": # @ada\n ttl: 3600\n type: NS\n values:\n' + " - ns1.example.com.\n", + "apex NS records belong to Cloudflare", + ) + + def test_ns_on_a_subdomain_is_fine(self): + body = ( + "---\nsub: # @ada\n ttl: 3600\n type: NS\n values:\n" + " - ns1.example.com.\n" + ) + self.assertEqual(self.check(body), []) + + def test_meta_record(self): + self.assertOneError( + "---\noctodns-meta: # @ada\n ttl: 120\n type: TXT\n value: x\n", + "`octodns-meta` is written by octoDNS", + ) + + def test_proxied_txt(self): + self.assertOneError( + "---\napi: # @ada\n octodns:\n cloudflare:\n proxied: true\n" + " ttl: 300\n type: TXT\n value: x\n", + "`api` TXT cannot be proxied", + ) + + def test_duplicate_name(self): + errors = self.check( + "---\napi: # @ada\n ttl: 300\n type: A\n value: 192.0.2.1\n\n" + "api: # @ada\n ttl: 300\n type: A\n value: 192.0.2.2\n" + ) + self.assertTrue( + any("`api` is already defined on line 2" in e for e in errors), errors + ) + + def test_errors_carry_the_line_number(self): + errors = self.check(GOOD.replace("api: # ada@patchworklabs.org, @grace", "api:")) + self.assertEqual(len(errors), 1, errors) + self.assertTrue(errors[0].startswith("patchworklabs.org.yaml:10:"), errors) + + +class CheckRepoTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.repo = Path(self.tmp.name) + (self.repo / "config").mkdir() + (self.repo / "config" / "config.yaml").write_text(CONFIG) + + def test_good_repo(self): + (self.repo / "patchworklabs.org.yaml").write_text(GOOD) + self.assertEqual(check_repo(self.repo), []) + + def test_missing_zone_file(self): + errors = check_repo(self.repo) + self.assertEqual(len(errors), 1, errors) + self.assertIn("the file is missing", errors[0]) + + def test_stray_yaml_file(self): + (self.repo / "patchworklabs.org.yaml").write_text(GOOD) + (self.repo / "patchworklabs.com.yaml").write_text(GOOD) + errors = check_repo(self.repo) + self.assertEqual(len(errors), 1, errors) + self.assertIn("patchworklabs.com.yaml:1: this file is not a zone", errors[0]) + + +class NightlySyncTest(unittest.TestCase): + """The file that the nightly sync writes must be readable by this check.""" + + def test_new_record_from_cloudflare_needs_an_owner(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "live").mkdir() + (root / "repo").mkdir() + (root / "repo" / "patchworklabs.org.yaml").write_text(GOOD) + live = GOOD + "\nnew:\n ttl: 300\n type: A\n value: 192.0.2.9\n" + (root / "live" / "patchworklabs.org.yaml").write_text(live) + + merge_zone( + "patchworklabs.org.", root / "live", root / "repo", + {"octodns-meta"}, "2026-09-21", + ) + errors = check_file(root / "repo" / "patchworklabs.org.yaml") + + self.assertEqual(len(errors), 1, errors) + self.assertIn("`new` has no owner", errors[0]) + + +if __name__ == "__main__": + unittest.main() From 75c36d77e1cd545dd71c6b2a0948e0a4d099d282 Mon Sep 17 00:00:00 2001 From: Jasper Mayone Date: Mon, 28 Sep 2026 16:47:39 -0400 Subject: [PATCH 2/2] fix: match zone files to live cloudflare and add owners --- hackathon.help.yaml | 16 ++++++++-------- patchworklabs.org.yaml | 42 ++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/hackathon.help.yaml b/hackathon.help.yaml index dccc31c..7aa6196 100644 --- a/hackathon.help.yaml +++ b/hackathon.help.yaml @@ -1,5 +1,5 @@ -"": - - ttl: 1 +"": # @patchworklabsorg/infra + - ttl: 120 type: MX values: - exchange: aspmx.l.google.com. @@ -12,7 +12,7 @@ preference: 10 - exchange: alt4.aspmx.l.google.com. preference: 10 - - ttl: 1 + - ttl: 120 type: TXT values: - google-site-verification=dWzvYUBk_oc6spUhOFmqPl8wYeRMvpfhhARS1tb21ag @@ -20,13 +20,13 @@ - v=spf1 include:_spf.google.com ~all - slack-domain-verification=hwBycY5FO958m1HWCSDHQIWCM6z566RY95Swq8qo -_dmarc: - ttl: 1 +_dmarc: # @patchworklabsorg/infra + ttl: 120 type: TXT - value: v=DMARC1\; p=quarantine\; rua=mailto:dmark_reports@hackathon.help\; pct=100\; ruf=mailto:dmark_reports@hackathon.help\; adkim=s\; aspf=s + value: v=DMARC1\; p=quarantine\; np=reject\; rua=mailto:dmarc_reports@hackathon.help\; ruf=mailto:dmarc_reports@hackathon.help\; adkim=s\; aspf=s # Google Workspace DKIM authentication -google._domainkey: - ttl: 3600 +google._domainkey: # @patchworklabsorg/infra + ttl: 120 type: TXT value: v=DKIM1\; k=rsa\; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1ZPaz12FXM3cVvaD96N1XbWsc1Um/EZYE8UMV+CQqupWmI/aqwfT5bQWHOCwp8RU+eI8ONee12ah7xnUThU4Ls+BNXyyrsRUAVfKowk0pxXpDwLHS0kf8sXBrsOLqQDwNOrSQ7P33YxhghExdwvdQ5O0qL557wjWU+zhbjiF1HzJm6Ved2Nya98cXu1UbkVGQsmlMJVb0nEZIvmD19sIxNkhXFUV6KIALqa7iai+YT+tapiiCc2XUzw4GTcqfIyS9leKn5Gz1gWCCgMCL3n03kxuzxR6PkS5YlgNZPur4MohsUU3UQZsAoF+NNoGTA67uY0HpLT91CVWuNfjMkTtFQIDAQAB diff --git a/patchworklabs.org.yaml b/patchworklabs.org.yaml index 2149495..d767ba0 100644 --- a/patchworklabs.org.yaml +++ b/patchworklabs.org.yaml @@ -1,5 +1,5 @@ -"": - - ttl: 1 +"": # @patchworklabsorg/infra + - ttl: 120 type: MX values: - exchange: aspmx.l.google.com. @@ -12,7 +12,7 @@ preference: 10 - exchange: alt4.aspmx.l.google.com. preference: 10 - - ttl: 1 + - ttl: 120 type: TXT values: - google-site-verification=OjTBuEBXcUdIOVzjidh1sCMfvAYvabpQWnkGfFFQQA4 @@ -23,61 +23,59 @@ - octodns: cloudflare: proxied: true - ttl: 1 + ttl: 120 type: A value: 216.198.79.1 -_atproto: - ttl: 1 +_atproto: # @patchworklabsorg/infra + ttl: 120 type: TXT value: did=did:plc:bpd7j2a34mmnyu7t64gzptg7 -_dmarc: - ttl: 1 +_dmarc: # @patchworklabsorg/infra + ttl: 120 type: TXT - value: v=DMARC1\; p=quarantine\; rua=mailto:dmark_reports@patchworklabs.org\; pct=100\; ruf=mailto:dmark_reports@patchworklabs.org\; adkim=s\; aspf=s + value: v=DMARC1\; p=quarantine\; np=reject\; rua=mailto:dmarc_reports@patchworklabs.org\; ruf=mailto:dmarc_reports@patchworklabs.org\; adkim=s\; aspf=s -_gh-patchworklabsorg-o: - ttl: 3600 +_gh-patchworklabsorg-o: # @patchworklabsorg/infra type: TXT value: cee2c56f89 -admin.forms: +admin.forms: # @patchworklabsorg/infra octodns: cloudflare: proxied: true - ttl: 1 + ttl: 120 type: A value: 65.19.76.238 -forms: +forms: # @patchworklabsorg/infra octodns: cloudflare: proxied: true - ttl: 1 + ttl: 120 type: A value: 65.19.76.238 # Google Workspace DKIM authentication -google._domainkey: - ttl: 3600 +google._domainkey: # @patchworklabsorg/infra type: TXT value: v=DKIM1\; k=rsa\; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzEuGHXgOxIj0qk83hV4ajl/OIpXbhE/MTKhXU1VZDBISO/+yBCQsVyq1j4F13tWxUpYLlw78OJl+7TlAyZ4llYY5z2Oj+EiAIQZCRBLmKN7zDpnbwbiM4hfam5vnhCvFwdgg0aKUY221T/Pe5Mjz11e0VtyOJ3D3enPId010bi99p93Dimf+rFo9YFwps7U/V4O5rWaRyG9snFcl9tshvKTgQ6OoHEvLbvQIU1QTiXR6oHAI5KP/8BzA26djgIKqNuHaU9S70KSVUH/9CBSAjHP50j4i4rGGEr6PopNjxQxN5owwu2jUDEIOrv13RLpNPISu1NaPCgMnGVyfsQ77yQIDAQAB -idp: - ttl: 1 +idp: # @patchworklabsorg/infra + ttl: 120 type: A value: 129.213.163.213 -openpgpkey: +openpgpkey: # @patchworklabsorg/infra ttl: 300 type: CNAME value: wkd.keys.openpgp.org. -www: +www: # @patchworklabsorg/infra octodns: cloudflare: proxied: true - ttl: 1 + ttl: 120 type: CNAME value: 52fd2b2210caa11f.vercel-dns-017.com.