(octo)DNS chez Numberly

L'histoire de la gestion des DNS et comment nous avons géré les enregistrements DNS sans friction dans un environnement multi-tenant chez Numberly

N’avez-vous jamais rêvé de manipuler les enregistrements DNS sans friction afin de pouvoir créer et modifier des entrées DNS avec la certitude de ne pas détruire votre production et ni de vous attirer les foudres de vos amis sysadmins ? Et notamment dans un environnement multi-tenant ? Voici comment nous avons transformé ce rêve en réalité.

Comme nombre d’entreprises Tech, Numberly compte plus d’une centaine de développeurs qui créent et modifient des logiciels tous les jours et souhaite leur donner le plus de liberté possible en éliminant un maximum de frictions avec les autres équipes. Surtout lorsque le sujet est aussi sensible que le DNS.

Mais si vous avez un minimum d’expérience en tant qu’administrateur système (ou si vous faites semblant), vous savez que les DNS sont parfois la cause de nombreux problèmes.

Laisser n’importe qui modifier vos enregistrements DNS requiert donc un minimum de connaissances et, même avec cette exigence, beaucoup de contrôles de bon sens.

Voici l’histoire derrière la gestion des DNS pour tous chez Numberly.

Avant 2010, l'ère de Bind

À l’époque, nous gérions une seule instance de Bind.

Pour administrer n’importe quelle instance de bind sans automatisation, vous avez besoin au minimum :

  • d’une authentification SSH
  • d’un utilisateur ayant la possibilité de modifier un fichier de zone
  • et surtout d’utilisateurs ayant un minimum de connaissances en DNS pour ne pas casser votre fichier de zone.

Pour ces raisons, l’équipe informatique interne était chargée de toutes les modifications DNS. Et même avec une bonne connaissance des DNS, des erreurs humaines continuaient à se produire car nous n’avions pas les fonctions de linting et de vérification dont disposent les autres serveurs DNS.

Avec l’essor de l’entreprise, ce processus manuel devenait de moins en moins adapté. Nous avons dû l’automatiser et résoudre nos problèmes d’erreurs humaines. Ainsi, au lieu d’écrire un script d’automatisation pour générer une configuration de Bind, avec la nécessité de mettre en oeuvre des contrôles d’intégrité (entre autres), nous nous sommes tournés vers PowerDNS et son serveur DNS autoritaire, livré avec une API.

Après 2010, l'ère PowerDNS

Voici PowerDNS, un outil fantastique développé par Open-XChange, la même société qui se cache derrière dovecot.

En tant que serveur DNS autoritaire, il vous donne la possibilité de gérer des milliers de fichiers de zone, soutenu par une API puissante et un stockage évolutif tel que les bases de données MySQL et PostgreSQL.

La migration s’est déroulée sans heurts car PowerDNS comprend des outils pour importer des fichiers de zone Bind.

À cette époque, pour rester aussi simple et sans dépendance externe, le backend qui a été choisi était sqlite. Il n’est pas évolutif et ne gère pas la réplication de manière native. Nous sommes donc tombés sur quelques problèmes par la suite.

Protéger notre DNS public des attaques DDoS

De plus, à cette époque, les attaques DDoS basées sur les DNS étaient vraiment courantes. Et gérer vos propres serveurs DNS pouvait rapidement devenir difficile car ils sont souvent la cible d’attaques.

Les protections DDoS sont assez coûteuses, et si vous ne disposez pas de systèmes de protection ou de solutions de surveillance et de logging suffisantes pour mettre en œuvre automatiquement la limitation de débit ou le bannissement d’IP avec des outils tels que dnsdist ou Crowdsec (oui, fail2ban est officiellement mort), vous pouvez rapidement rencontrer des problèmes…

C’est une des raisons pour lesquelles nous avons décidé d’héberger ces serveurs PowerDNS en dehors de notre réseau chez un fournisseur de cloud public avec une protection DDoS.

Plus tard, une instance dnsdist a été mise en place au dessus de PowerDNS pour limiter le débit et éviter trop de problèmes, cela s’est avéré efficace. Très efficace même, croyez-moi.

Voici un exemple de configuration dnsdist que nous avons utilisé :

-- tuning
setMaxUDPOutstanding(65000)

controlSocket('127.0.0.1:5199')
setKey("sup3rm4g4s3cur3k3y")
-- we should create as much addLocal() as we have CPU for intense workloads
addLocal('<our_public_ip>:53', {doTCP=true, reusePort=true})

-- allow all to recurse us
setACL("0.0.0.0/0")
newServer{address="<pdns_1_ip>", qps=10000, name="pdns-1", useClientSubnet=true, checkType="A", checkName="www.numberly.com.", mustResolve=true}
newServer{address="<pdns_2_ip>", qps=10000, name="pdns-2", useClientSubnet=true, checkType="A", checkName="www.numberly.com.", mustResolve=true}

setServerPolicy(roundrobin)

-- Drop ANY queries
addAction(QTypeRule(dnsdist.ANY), DropAction())

-- Apply Rate Limit for NXDomain and ServFail queries
local dbr = dynBlockRulesGroup()
dbr:setRCodeRate(dnsdist.NXDOMAIN, 5, 10, "Exceeded NXD rate", 60, DNSAction.Drop)
dbr:setRCodeRate(dnsdist.SERVFAIL, 5, 10, "Exceeded ServFail rate", 60, DNSAction.Drop)
dbr:excludeRange({"127.0.0.1/32", "10.0.0.0/8" })

function maintenance()
  dbr:apply()
end

 

Quelques années plus tard, avec l’utilisation croissante de Kubernetes chez Numberly, nous avons dû connecter Kubernetes à PowerDNS afin de permettre à nos développeurs d’exposer leurs applications Web directement à partir de leur configuration de déploiements Kubernetes.

Mais l’un des inconvénients de l’API de PowerDNS est qu’elle n’est pas multi-tenant, PowerDNS vous donne une clé d’API unique.

Corriger l'absence de multi-tenant de PowerDNS

Comme PowerDNS ne fournit qu’une clé API unique, nous avons voulu résoudre ce problème pour améliorer la sécurité des accès à notre API.

Nous avons utilisé un reverse-proxy nginx devant notre API PowerDNS avec une liste d’autorisations configurée par Ansible. Cela ne fait que résoudre les problèmes d’authentification et rien de plus.

Nous avons envisagé de mettre en place des flux basés sur des ACL, par exemple en liant des zones DNS à une clé d’API, mais c’était assez excessif dans notre situation.

Les extraits de configuration parlent d’eux-mêmes :

map $http_x_api_key $key {
    default 0;
    sup3rm4g4s3cur3k3y       1; # key for k8s-pa7-1-external-dns
    m3g4sup3rm4g4s3cur3k3y   1; # key for k8s-par5-1-external-dns
    ultr4sup3rm4g4s3cur3k3y3 1; # key for octodns
}

server
{
    listen      443 ssl;
    server_name pdns.acme.internal

    ssl_certificate /etc/ssl/cert.crt;
    ssl_certificate_key /etc/ssl/cert.key;

    access_log /var/log/nginx/pdns.log main;
    error_log /var/log/nginx/pdns.log error;

    if ($key = 0) {
      return 403;
    }

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-API-Key <YOUR_UNIQUE_PDNS_API_KEY>;
        proxy_pass       http://127.0.0.1:8081/;
    }
}

L'ère Covid 2020, AWS Route53

Lorsque vos besoins et l’échelle de vos zones DNS sont assez importants, la gestion de votre DNS public peut devenir chronophage, même avec beaucoup d’automatisation.

La gestion de nos instances publiques PowerDNS prenait de plus en plus de temps, alors au lieu de gaspiller notre énergie à essayer de devenir un fournisseur d’hébergement, nous voulions simplement nous concentrer sur l’utilisation des DNS. Nous avons également pensé qu’il était temps de mieux suivre les modifications apportées à nos enregistrements DNS.

Pourquoi ne pas revenir à des choses simples ? Comme git, ansible et une bonne vieille pipeline CI ?

Une solution basée sur le cloud répondait clairement à ces objectifs grâce à des centaines d’ingénieurs dans plusieurs entreprises de cloud qui automatisent ce genre de problèmes pour des milliers d’entreprises comme nous. Nous avons choisi AWS Route53 pour y parvenir.

Première tentative d'utilisation du module route53 d'ansible

Nous avons commencé par utiliser ce que nous connaissions le mieux et qui nous semblait naturel, mais nous nous sommes vite découragés parce que l’implémentation du module d’ansible route53 fait un applel HTTP à AWS par entrée configurée. Il n’exploite pas les points de terminaison en bulk de l’API Route53 !

De plus, il n’implémente pas d’idempotence, qui est un pilier technologique de Numberly.

Avec plus de 900 zones totalisant plus de 50 000 enregistrements DNS, il fallait 23 heures pour exécuter ansible. Ce n’était clairement pas à la hauteur de nos besoins.

Par ailleurs, vous devez tenir compte du fait qu’AWS limite vos appels à l’API Route53 à seulement 5 requêtes par seconde.

Comment pourrions-nous déplacer toutes nos zones DNS vers Route53 avec une telle limitation ?

Seconde tentative d'utilisation d'octoDNS

Voici octoDNS, un outil développé par Github pour résoudre le problème exact auquel nous étions confrontés (merci l’Open Source) !

Considéré comme “DNS as code”, octoDNS gère plusieurs fournisseurs et vous permet de les utiliser comme entrée, sortie ou les deux.

Vous trouverez ci-dessous un exemple de configuration d’octoDNS :

manager:
  max_workers: 1
providers:
  yaml:
    class: octodns.provider.yaml.YamlProvider
    directory: ./records
  route53:
    class: octodns.provider.route53.Route53Provider
  ...

zones:
  numberly.com.:
    sources:
    - yaml
    targets:
    - route53
  ...

 

Déclarez votre zone :

datalively:
- ttl: 86400
  type: A
  value: 195.66.82.254
- ttl: 86400
  type: MX
  values:
  - exchange: tsunami1.mm-send.com.
    preference: 5
  - exchange: tsunami2.mm-send.com.
    preference: 5
- ttl: 86400
  type: SPF
  value: v=spf1 include:mm-send.com -all
- ttl: 86400
  type: TXT
  values:
  - spf2.0/pra include:mm-send.com -all
  - v=spf1 include:mm-send.com -all

 

Exécutez votre changement sur chaque approbation de fusion-demande avec un .gitlab-ci.yaml :

Update DNS:
  stage: deploy
  script: "octodns-sync --config-file=./config.yaml --doit"
  resource_group: production
  only:
    refs:
      - master
    changes:
      - config.yaml
      - records/*
  except:
    - schedules
  retry: 2

Migrer de PowerDNS vers octoDNS

Notre première action a été de gérer la migration de la base de données sqlite de notre gros PowerDNS vers AWS Route53.

Nous avons écrit un script pour générer des fichiers .yaml par nom de domaine, nous les avons nettoyés et déployés.

  • Entrée : yaml
  • Sortie : Route53

Notre script a généré ce type de sortie de configuration octoDNS :

providers:
  route53:
    class: octodns.provider.route53.Route53Provider
    max_changes: 100
  yaml:
    class: octodns.provider.yaml.YamlProvider
    default_ttl: 3600
    directory: ./records
    enforce_order: true
    populate_should_replace: true\

zones:
  1ldb.fr.:
    sources:
    - yaml
    targets:
    - route53

 

Pour plus de 900 zones, il a fallu moins de 5 minutes à octoDNS pour effectuer la synchronisation sans déclencher de limite de débit AWS Route53 en utilisant les méthodes bulk de l’API !

Nous n’en revenions pas de la simplicité et de l’efficacité de l’outil 🙂

Après de nombreuses vérifications (nous n’arrivions vraiment pas à y croire !), nous avons validé son bon fonctionnement et avons rapidement déployé ce projet. Nous en étions très heureux.

Pendant des mois, des dizaines d’utilisateurs et de développeurs de Numberly sur Gitlab ont créé des demandes des Merge Requests pour mettre à jour notre DNS. Git s’est avéré utile pour suivre et comprendre quand, pourquoi et qui a modifié chaque enregistrement DNS.

Le seul problème qui subsistait était que ce changement structurel supprimait la possibilité pour nos développeurs de créer des entrées DNS par le biais du module bien connu de Kubernetes external-dns.

Il a été configuré pour écrire directement sur les zones Route53 d’AWS, mais à cause de l’idempotence d’octoDNS qui supprime tous les enregistrements qu’il ne connaît pas, cela n’a pas bien fonctionné…

Jumeler octoDNS et external-dns

À ce stade, nos DNS n’étaient plus hébergés sur notre infrastructure et nous disposions d’une API puissante, mais dont le coût était limité.

La seule chose qui restait à résoudre était de savoir comment l’intégrer à nos autres méthodes de déploiement, comme notre intégration de Kubernetes à external-dns.

Nous avons conservé un cluster PowerDNS uniquement interne pour servir les zones internes. Nous l’avons donc envisagé pour héberger, sans limite de débit, des zones DNS non exposées qui pourraient être fusionnées avec AWS Route53, par octoDNS.

La vue d’ensemble du pipeline DNS qui en résulte :

  • Entrée : PowerDNS puis yaml
  • Sortie : Route53

Nous avons ajouté la configuration octoDNS suivante :

providers:
  pdns:
    api_key: env/PDNS_API_KEY
    class: octodns.provider.powerdns.PowerDnsProvider
    host: pdns.internal.acme
    port: 443
    scheme: https
  route53:
    class: octodns.provider.route53.Route53Provider
    max_changes: 100
  yaml:
    class: octodns.provider.yaml.YamlProvider
    default_ttl: 3600
    directory: ./records
    enforce_order: true
    populate_should_replace: true
  numberly.com.:
    sources:
    - pdns
    - yaml
    targets:
    - route53

 

Côté Kubernetes :

 - args:
    - --domain-filter=numberly.com
    - --interval=30s
    - --log-level=debug
    - --policy=sync
    - --provider=pdns
    - --source=ingress
    - --pdns-server=https://pdns.acme.internal
    - --pdns-api-key=sup3rm4g4s3cur3k3y
    image: bitnami/external-dns:0.7.6

 

Avec cette configuration, octoDNS fusionne PowerDNS et nos enregistrements YAML basés sur git, dans cet ordre.

Cette pipeline DNS a l’agréable effet secondaire de s’assurer que tout enregistrement DNS dynamique provenant de PowerDNS sera remplacé par les enregistrements YAML basés sur git, empêchant ainsi toute erreur de l’utilisateur qui pourrait écraser un enregistrement DNS existant (même si external-dns gère déjà ce cas).

NetOps + DNS = ❤️

Chez Numberly, nous utilisons beaucoup un excellent outil Open Source appelé Netbox pour gérer l’infrastructure de notre Datacenter et nos adresses IP grâce à sa puissante API.

À l’heure actuelle, notre équipe réseau a fini de déployer le tout nouveau réseau Arista de notre centre de données.

Ayant fini d’automatiser les configurations du réseau avec ansible et Netbox, ils ont cherché à automatiser la création et les mises à jour de leurs DNS inversés.

Cela a été facilement réalisé en écrivant un module qui récupère l’attribut DNS inversé de l’IP à partir de l’API Netbox. Une fois encore, un plugin Open Source existait : octodns-netbox.

  • Entrée : Netbox puis yaml
  • Sortie : Route53 puis powerDNS (pour l’interne)

Nous avons donc configuré octoDNS comme tel :

providers:
  pdns:
    api_key: env/PDNS_API_KEY
    class: octodns.provider.powerdns.PowerDnsProvider
    host: pdns.internal.acme
    port: 443
    scheme: https
  netbox:
    class: octodns_netbox.NetboxSource
    url: https://netbox.internal.acme/api
    token: env/NETBOX_TOKEN
  route53:
    class: octodns.provider.route53.Route53Provider
    max_changes: 100
  yaml:
    class: octodns.provider.yaml.YamlProvider
    default_ttl: 3600
    directory: ./records
    enforce_order: true
    populate_should_replace: true

  82.66.195.in-addr.arpa.:
    sources:
    - netbox
    - yaml
    targets:
    - route53

 

En quelques heures, nous avons automatisé tous nos sous-réseaux publics et nos sous-réseaux internes pour les interconnexions de routeurs !

Nos contributions à l'Open Source

Il est rare que l’on suive ce genre de chemin sans heurter quelques pierres. La beauté de l’Open Source, à laquelle nous croyons fermement, c’est que vous pouvez transformer ces obstacles en le bien de toute la communauté grâce à des contributions !

Rejoignez nos équipes