kong auf Docker via IPv6

Kürzlich konnte ich meine eigenen Online-Services nicht erreichen – scheinbar weil ich in einem IPv6-only Netz “unterwegs” war und so IPv4-Dienste nicht funktionierten. So gewann ich dein Eindruck, es könnte sich lohnen, mal ein wenig IPv6 lernen. Bei mir läuft extern alles via kong – also musste ich kong auf docker via IPv6 erreichbar machen.

Vorab ein “Disclaimer”: Ich kann (wie eigentlich immer) nicht behaupten, alles vollständig verstanden zu haben. Dies sind meine Beobachtungen und Notizen, wie ich es zum Laufen bekommen habe:

Basics: 3 Arten Container via IPv6 erreichbar zu machen

Es gibt 3 Wege, Container via IPv6 erreichbar zu machen (alle 3 sind auch hier genauer beschrieben):

  1. Über den “userland proxy” (docker-proxy), der bei Docker-Port-Freigaben automatisch konfiguriert wird. Er wird z.B. auf 0.0.0.0 bzw. [::] hören, wenn in docker-compose bei den Ports z.B. - 80:80 steht. Das ist ja bei IPv4 der “ganz normale” Weg.
    Großer Nachteil in meinen Augen: Bei IPv6 kommen aus Sicht des Containers dann aber alle Requestst von der IP des docker-proxy.

  2. Über einen Hilfs-Container aus robbertkl/ipv6nat: Das konfiguriert iptables -6 genauso als NAT, wie man das auch von IPv4 kennt.
    Aber auch das gefällt mir nicht: Erstens brauch ich dann immer noch einen zusätzlichen Container, von dessen Maintainer ich abhängig bin und zweitens ist NAT nicht sehr “IPv6-like”.

  3. Routing via Host direkt zum Container.

Daher habe ich mich für den letzgenannten Weg entscheiden:

1. Docker Host mit IPv6 versorgen

Als erstes muss natürlich der Host selber eine IPv6 bekommen. Von meinem Hoster habe ich ein 64er Prefix erhalten (2a03:2222:3333:4444::/64). Darin habe ich mir eine IP ausgesucht, die die IP des Servers sein soll.

  1. Auf meinem Ubuntu-Server ging das so: In der /etc/netplan/01-netcfg.yaml:
    ...
    ens3:
      dhcp4: no
      addresses:
        - 1.2.3.4/22
        - 2a03:2222:3333:4444::2/64
      gateway4: 1.2.3.1
      gateway6: fe80::1
    ...
    
  2. netplan apply

  3. Beim Hoster den Reverse Lookup eintragen.

2. Docker IPv6 beibringen

Docker verwaltet ja “jede Menge” eigene Netze, in denen bisher nur IPv4 gesprochen wurde. Die sollen jetzt grundsätzlich auch IPv6 können.

  1. In der /etc/docker/daemon.json:

    Hier kann man sich entscheiden, ob man als Default-IPv6 schon die öffentlichen einträgt oder “interne”, nicht routbare. Ich habe mich gegen die öffentlichen entschieden, weil die allermeisten meiner Docker-Netze nicht öffentlich erreichbar sein sollen. Nur Kong wird öffentlich erreichbar, indem ich ein spezielles öffentliches Netz “ipv6_exposed” definiere (s.u.). So habe in einen Beitrag zum Schutz vor versehentlichem Veröffentlichen von internen Services.

    ...
    "ipv6": true,
    "fixed-cidr-v6": "fd00::/80" # oder: "2a03:2222:3333:4444:300::/72",
    ...
    
  2. systemctl reload docker

3. Öffentliches Docker-Netz definieren

Wie schon geschrieben, habe ich mich entschieden, nur genau eins meiner Docker-Netze öffentlich zu machen, damit ich bei jedem Docker-Service entscheiden kann und muss, ob ich ihn bewusst in dieses Netz lege, um ihn dadurch öffentlich zu machen.

docker network create --ipv6 --subnet=2a03:2222:3333:4444:300::/80 ipv6_exposed

Dieses Netz wird hiermit als 80er-Netz definiert und erzeugt. Es bleibt persistent bestehen und muss bei Bedarf explizit gelöscht werden.

4. Docker-Netz extern erreichbar machen

Ich nutze den Neighbor Discovery Protocol Proxy Daemon (ndppd), um mein eigenes, gerade angelegtes öffentliches Netz von extern erreichbar zu machen. Dieser Dämon beantwortet Nachbar-Anfragen (Neighbor Solicitations) mit den entsprechenden Antworten (Neighbor Advertisements). So erfährt und speichert der Router des Hosters z.B. welche anderen Netze über meinen Host als Router erreichbar sind.

  1. apt install ndppd
  2. In der /etc/ndppd.conf:
    proxy ens3 {
              rule 2a03:2222:3333:4444:300::/80 {
                auto
              }
           }
    

Damit deklariere ich das Netz als über meinen Host erreichbar. Der ndppd macht das bei seinen Nachbarn bekannt. ens3 ist das Netz, worüber der Host von außen erreichbar ist und wo also der ndppd aktiv sein soll.

5. Container dem öffentlichen Docker-Netz zuordnen

In der docker-compose.yml definiere ich ein internes (IPv4 only) Netz und außerdem das öffentliche ipv6_exposed, das ich oben erzeugt habe:

...
networks:
 kong-net:
  driver: bridge
  enable_ipv6: false
  ipam:
    config:
      - subnet: 172.99.0.0/16
        gateway: 172.99.0.1
 ipv6_exposed:
  external:
   name: ipv6_exposed
...

Das Keyword external gibt an, dass das Netz außerhalb des Compose-Files erzeugt wurde – also schon existieren muss.

6. Kong IPv6 beibringen

Da Kong nun direkt selber erreichbar ist und kein Docker mehr eine Port-Umsetzung macht (wie bei es bei IPv4 üblich ist), muss er auch selber direkt auf die Ports 80 und 443 hören. Dazu…

  1. braucht Kong die entsprechenden Rechte (Capabilities):
    Ich geb sie ihm im Dockerfile, in dem ich mir auch diverse Kong-Plugins installiere:

    FROM kong:2.3
    USER root
    # install some plugins
    ...
    # see https://github.com/Kong/docker-kong/blob/60626098f2f32fe1528eb4ffacff13fd1c3e919f/alpine/Dockerfile
    # https://blog.container-solutions.com/linux-capabilities-in-practice
    RUN setcap 'cap_net_bind_service+eip' /usr/local/openresty/nginx/sbin/nginx
    USER kong
    
  2. Nun kann er auf privilegierte Ports (unter 1024) selber hören. Im docker-compose.yml:
      ...
      kong:
        build: ./kong-build
        restart: always
        networks:
          kong-net:
            ipv4_address: 172.99.0.100
          ipv6_exposed:
            ipv6_address: ${KONG_BIND_IPV6:?err}
        cap_add:
          - NET_BIND_SERVICE
        environment:
          KONG_PG_HOST: kong-database
          KONG_PG_DATABASE: kong
          KONG_PG_USER: kong
          KONG_PG_PASSWORD: ${KONG_PG_PASSWORD:-kong}
          KONG_PROXY_LISTEN: 0.0.0.0:80, 0.0.0.0:443 ssl, [::]:443 ssl, [::]:80
          KONG_ADMIN_LISTEN: 0.0.0.0:8001
          KONG_PROXY_ACCESS_LOG: /dev/stdout
          KONG_ADMIN_ACCESS_LOG: /dev/stdout
          KONG_PROXY_ERROR_LOG: /dev/stderr
          KONG_ADMIN_ERROR_LOG: /dev/stderr
        depends_on:
          - kong-migration
          - kong-database
        volumes:
          - ${KONG_DATA_DIR:?err}/logs:/var/log
        healthcheck:
          test: ["CMD", "curl", "-f", "http://kong:8001"]
          interval: 50s
          timeout: 2s
          retries: 15
        ports:
          - ${KONG_BIND_IP:?err}:80:80
          - ${KONG_BIND_IP:?err}:443:443
    

    Beachte: Die KONG_BIND_IPV6 ist nicht unter ports zu finden. Dies würde den Userland-Proxy konfigureren. Das mach ich nicht; der Container hat ja seine eigene IPv6 und hört selber auf die entsprechenden Ports (siehe oben).
    Die KONG_BIND_IP und KONG_BIND_IPV6 stehen im .env:

    ...
    KONG_BIND_IP=1.2.3.4
    KONG_BIND_IPV6=2a03:4000:43:60:300::8100
    
  3. Nun nur noch im DNS den AAAA-Record 2a03:2222:3333:4444:300::8100 eintragen.

7. Forwarding und Kernel-Firewall (iptables/ufw)

  1. Außerdem muss IP-Forwarding für IPv6 aktiviert sein. Bei mir war/ist das bereits. Ich weiß nicht, ob Docker das vielleicht aktiviert.

    # sysctl net.ipv6.conf.all.forwarding
    net.ipv6.conf.all.forwarding = 1
    

    Ggf. in /etc/sysctl.d/99-sysctl.conf einkommentieren.

  2. Dementsprechend ist logisch, dass die Pakete zu den Containern über die Forward-Tables geroutet werden und z.B. ufw so eingestellt werden muss:

    # ufw status
    ...
    443/tcp                    ALLOW FWD   Anywhere
    443/tcp (v6)               ALLOW FWD   Anywhere (v6)
    ...
    

Fazit

Das ganze läuft bei mir nun seit ein paar Monaten stabil und ohne dass ich nochmal “nachjustieren” musst. Auch ein Docker-Versionsupgrade hat das ganze schon “überlebt”.

Anhang: Links

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.