pdns icon indicating copy to clipboard operation
pdns copied to clipboard

docker: provide non-commandline non-mount method for passing settings and db credentials

Open HaleyACS opened this issue 4 years ago • 26 comments

  • Program: Authoritative, Recursor, dnsdist
  • Issue type: Bug report

Short description

When using the docker images, one needs to configure the database backend at container start. Currently it is not possible to pass mysql configuration parameters through the docker-compose file (for example) to the image, so that at start of the container the connection configuration to the database backend is set. In docker, the container is ephemeral. Means that once you stop and delete the container, all data that was modified inside is gone. The same applies in using the powerdns in kubernetes. Even though kubernetes probides the possibility to use configmaps, these would expose the password in a generic way. Using secret configuration enables the admin to securely configure the database access without exposing the passwords in a environment variable.

Environment

  • Operating system: docker/containerd
  • Software version: any version / docker 20.10.20, Kubernetes 1.20
  • Software source: Powerdns official docker repositories: https://hub.docker.com/r/powerdns/pdns-auth-44

Steps to reproduce

  1. Try to load the docker container in an environment, one needs to manually configure/manipulate the container to access the DB backend. Alternatively, expose access-passwords in scripts/environments which make these unsuitable for production use.

Expected behaviour

Have some code in the entrypoint script that is able to extract the DB configuration out of the environment (docker/docker-compose).

Actual behaviour

Currently the docker image is not usable with database (mysql for example) backend out of the box.

Other information

Add code to the entry-point script to extract basic configuration parameters so that the image can run in a docker env. with database backend. Various techniques exist.

HaleyACS avatar Jan 11 '21 10:01 HaleyACS

Currently it is not possible to pass mysql configuration parameters through the docker-compose file (for example) to the image, so that at start of the container the connection configuration to the database backend is set.

This is possible via command line arguments, but I can understand that that is not preferred!

Habbie avatar Jan 11 '21 10:01 Habbie

Add code to the entry-point script to extract basic configuration parameters so that the image can run in a docker env. with database backend. Various techniques exist.

This exists btw, it is not documented. You'll need to bring your own templates (into /etc/pdns/templates.d/*.j2), the entrypoint will parse these templates and dump the generated config to the right directory

pieterlexis avatar Jan 11 '21 10:01 pieterlexis

Add code to the entry-point script to extract basic configuration parameters so that the image can run in a docker env. with database backend. Various techniques exist.

This exists btw, it is not documented. You'll need to bring your own templates (into /etc/pdns/templates.d/*.j2), the entrypoint will parse these templates and dump the generated config to the right directory

OK. I saw something about it in the pdns_server-startup script. Even though I don't like to run python Eventually that should go into the documentation then. What is a *.j2 file? Could you provide me a working example? Thx.

HaleyACS avatar Jan 11 '21 10:01 HaleyACS

What is a *.j2 file?

Jinja2 template

Could you provide me a working example? Thx.

FROM powerdns/pdns-auth-master:latest

ADD templates/etc/powerdns/templates.d etc/powerdns/templates.d

Template (main.j2):

local-address=0.0.0.0:5300

{% if pdns_query_local_address | size > 0 %}
query-local-address={{ pdns_query_local_address }}
{% endif %}

{% if pdns_server_id | size > 0 %}
server-id={{ pdns_server_id }}
{% endif %}

{% if pdns_version_string %}
version-string={{ pdns_version_string }}
{% endif %}

{% if pdns_webserver_address %}
webserver-address={{ pdns_webserver_address }}
{% endif %}

{% if pdns_webserver_port %}
webserver-port={{ pdns_webserver_port }}
{% endif %}

{% if pdns_webserver_allow_from %}
webserver-allow-from={{ pdns_webserver_allow_from }}
{% endif %}

{% if pdns_api_key %}
api=yes
api-key={{ pdns_api_key }}
{% endif %}

{% if pdns_lua_records %}
enable-lua-records={{ pdns_lua_records }}
{% endif %}
---
version: '3'
services:
  auth:
    environment:
      - TEMPLATE_FILES=main
      - pdns_server_id="my server"
      - pdns_webserver_port="8081"
      - pdns_api_key="CHANGEME"
    image: ${IMAGE}

pieterlexis avatar Jan 11 '21 11:01 pieterlexis

Thanks. Great stuff. Do you know from whihc version on this is included? default packages for alpine are 4.2.3. Thx.

HaleyACS avatar Jan 11 '21 13:01 HaleyACS

It's included in our Docker images, which we started doing just before 4.4.0. It's not in the dist tarballs, but we could change that for 4.4.1/4.4.2.

Habbie avatar Jan 11 '21 13:01 Habbie

OK. I'll write my own workaround until alpine uses a 4.4.x release where one can use the template release.

You should definitely add the templates to the regular builds (alpine etc.) or make it an extra templates package so one can install these from ther official repos. This way, one could use the "locations" of the files to actually enable what is required at dep loyment time.

HaleyACS avatar Jan 11 '21 14:01 HaleyACS

We don't manage the alpine builds; what we can do is

  1. add it to the tarballs
  2. add it to the things that go into /usr/share/doc when you make install
  3. make additional packages

(1) and (2) make sense to me; I'd say (3) is not useful after that.

Habbie avatar Jan 11 '21 14:01 Habbie

Ok. 1 and 2 sound good. Make sure there are instructions on how to use that :+1:

HaleyACS avatar Jan 11 '21 14:01 HaleyACS

Perhaps you can provide those, now that you've seen it all. A PR to the Docker-README file would be best :)

Habbie avatar Jan 11 '21 14:01 Habbie

As I'm going with what is currently provided by alpine, I'm going with the old release to avoid having to recompile everything everytime I update a thing (even though we could do that, keeping the image up to date is easiest done using the provided package manager of the used base OS - trust me, I am with redhat since v1, and poked quite a lot with SLS back in time :) even though I use ubuntu LTS and apine nowadays)

HaleyACS avatar Jan 11 '21 14:01 HaleyACS

@HaleyACS Can you confirm that the templates actually work? I am trying to have a template allow for a postgres connection but I am running into issues. I am not sure that the template is not being written out to pdns.conf.

My template looks like:

pdns.j2

local-address=0.0.0.0,::

launch=gpgsql

{% if PGSQL_HOST %}
gpgsql-host={{ PGSQL_HOST }}
{% endif %}

{% if PGSQL_PORT %}
gpgsql-port={{ PGSQL_PORT }}
{% endif %}

{% if PGSQL_DATABASE %}
gpgsql-dbname={{ PGSQL_DATABASE }}
{% endif %}

{% if PGSQL_USERNAME %}
gpgsql-user={{ PGSQL_USERNAME }}
{% endif %}

{% if PGSQL_PASSWORD %}
gpgsql-password={{ PGSQL_PASSWORD }}
{% endif %}

{% if PGSQL_DNSSEC %}
gpgsql-dnssec={{ PGSQL_DNSSEC }}
{% endif %}

include-dir=/etc/powerdns/pdns.d

Then I am using a custom Dockerfile pulling from the base to include the template like @pieterlexis example.

The my docker-compose.yml looks like:

version: "3"
services:
  pdns:
    image: pdns:postgres
    environment:
      - PGSQL_HOST=pdns-db
      - PGSQL_PORT=5432
      - PGSQL_USERNAME=pdns
      - PGSQL_PASSWORD=pdns
      - PGSQL_DATABASE=pdns
      - PGSQL_DNSSEC=yes
      - TEMPLATE_FILES=pdns
    volumes:
      - /tmp/powerdns-testing/:/tmp/powerdns/

When I run docker-compose up I get the following logs:

pdns_1  | Feb 01 21:42:44 Loading '/usr/local/lib/pdns/libgpgsqlbackend.so'
pdns_1  | Feb 01 21:42:44 This is a standalone pdns
pdns_1  | Feb 01 21:42:44 Listening on controlsocket in '/var/run/pdns/pdns.controlsocket'
pdns_1  | Feb 01 21:42:44 Fatal error: Trying to set unknown parameter 'gsqlite3-dnssec'

It seems like the template might of worked as Loading '/usr/local/lib/pdns/libgpgsqlbackend.so' the postgres backend seems to be loaded instead of sqlite but the service gets hung up on gsqlite3-dnssec. I am not sure where gsqlite3-dnssec would be getting set other than the default pdns.conf.

@Habbie any idea where gsqlite3-dnssec could be set? Also, as it currently stands to get postgres to work I would need to source the default scheme. Currently, even with using the template, there is not way to source the scheme before starting the PowerDNS service. My above example would work fine with an existing database but would not work if it was a fresh install. Is there away to run another init script before launching the service?

james-crowley avatar Feb 01 '21 21:02 james-crowley

hmm, you don't see any output from print("Created {} with content:\n{}\n".format(target, rendered)) in the startup wrapper?

Habbie avatar Feb 01 '21 21:02 Habbie

@Habbie No I do not. I noticed by hacking around with the pdns_server-startup if I remove the last line os.execv(program, [program]+args+sys.argv[1:]), I can see the print statements just fine. But once the line gets added back in the prints are gone.

I wonder if its a buffer issue with python and calling another command inside the same script. You can check out this link. I had a similar issue with Python and Docker before. If you add a -u to the parameters when calling the script the output might show.

james-crowley avatar Feb 01 '21 22:02 james-crowley

Ah, good call, python might not be smart enough to flush buffers on execv.

Habbie avatar Feb 01 '21 22:02 Habbie

@Habbie Disabling the last line with a custom start-up wrapper I can see:

pdns_1      | Created /etc/powerdns/pdns.d/pdns.conf with content:
pdns_1      | local-address=0.0.0.0,::
pdns_1      |
pdns_1      | launch=gpgsql
pdns_1      |
pdns_1      |
pdns_1      | gpgsql-host=pdns-db
pdns_1      |
pdns_1      |
pdns_1      |
pdns_1      | gpgsql-port=5432
pdns_1      |
pdns_1      |
pdns_1      |
pdns_1      | gpgsql-dbname=pdns
pdns_1      |
pdns_1      |
pdns_1      |
pdns_1      | gpgsql-user=pdns
pdns_1      |
pdns_1      |
pdns_1      |
pdns_1      | gpgsql-password=pdns
pdns_1      |
pdns_1      |
pdns_1      |
pdns_1      | gpgsql-dnssec=yes
pdns_1      |
pdns_1      |
pdns_1      | include-dir=/etc/powerdns/pdns.d
pdns_1      |

But of course disabling the last line causes the pdns service not to start. I am not sure how to enable unbuffered prints and interact with tini. Usually the entry point would be something like this: ENTRYPOINT ["python","-u","main.py"].

Also with a custom wrapper I tried to copy the files from /etc/powerdns to /tmp/powerdns than use a Docker mount to see the files on my local filesystem to verify the file is getting written out. When I do that, pdns.conf is the same as the default config. But this could be due to the fact I am doing something wrong or out of order. All I did was add os.system("cp -r /etc/powerdns /tmp/powerdns") before the os. execv.

james-crowley avatar Feb 01 '21 22:02 james-crowley

I did not know -u was supported on python3, but it is! Let's use it

pieterlexis avatar Feb 02 '21 07:02 pieterlexis

@Habbie and @pieterlexis I think the templating issue has to do with permissions. From adding in @pieterlexis -u fix, I can now see the output. After adding in my own custom startup script, I added in some extra steps like adding in a "temp" file called pdns.conf.tmp. Plus I added in an ls -lah to check the permissions and to see if my "temp" file was there.

Here is the output:

pdns_1  | TEMPLATES: pdns
pdns_1  | Created /etc/powerdns/pdns.d/pdns.conf with content:
pdns_1  | local-address=0.0.0.0,::
pdns_1  |
pdns_1  | launch=gpgsql
pdns_1  |
pdns_1  |
pdns_1  | gpgsql-host=pdns-db
pdns_1  |
pdns_1  |
pdns_1  |
pdns_1  | gpgsql-port=5432
pdns_1  |
pdns_1  |
pdns_1  |
pdns_1  | gpgsql-dbname=pdns
pdns_1  |
pdns_1  |
pdns_1  |
pdns_1  | gpgsql-user=pdns
pdns_1  |
pdns_1  |
pdns_1  |
pdns_1  | gpgsql-password=pdns
pdns_1  |
pdns_1  |
pdns_1  |
pdns_1  | gpgsql-dnssec=yes
pdns_1  |
pdns_1  |
pdns_1  | include-dir=/etc/powerdns/pdns.d
pdns_1  |
pdns_1  | Creating temp file.....
pdns_1  | List directory of /etc/powerdns/
pdns_1  | total 52K
pdns_1  | drwxr-xr-x 1 root root 4.0K Feb  1 13:41 .
pdns_1  | drwxr-xr-x 1 root root 4.0K Feb  2 15:12 ..
pdns_1  | -rw-r--r-- 1 root root 3.1K Feb  1 13:41 ixfrdist.example.yml
pdns_1  | -rw-r--r-- 1 root root  139 Feb  1 13:21 pdns.conf
pdns_1  | -rw-r--r-- 1 root root  18K Feb  1 13:41 pdns.conf-dist
pdns_1  | drwxr-xr-x 1 pdns pdns 4.0K Feb  2 15:12 pdns.d
pdns_1  | drwxr-xr-x 1 pdns pdns 4.0K Feb  1 21:32 templates.d
pdns_1  | Printing /etc/powerdns/pdns.conf......
pdns_1  | local-address=0.0.0.0,::
pdns_1  | launch=gsqlite3
pdns_1  | gsqlite3-dnssec
pdns_1  | gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
pdns_1  | include-dir=/etc/powerdns/pdns.d
pdns_1  | Removing /etc/powerdns/pdns.conf......
pdns_1  | rm: cannot remove '/etc/powerdns/pdns.conf': Permission denied
pdns_1  | Printing /etc/powerdns/pdns.conf......
pdns_1  | local-address=0.0.0.0,::
pdns_1  | launch=gsqlite3
pdns_1  | gsqlite3-dnssec
pdns_1  | gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
pdns_1  | include-dir=/etc/powerdns/pdns.d
pdns_1  | Printing /etc/powerdns/pdns.conf.temp......
pdns_1  | cat: /etc/powerdns/pdns.conf.temp: No such file or directory
pdns_1  | List directory of /etc/powerdns/
pdns_1  | total 52K
pdns_1  | drwxr-xr-x 1 root root 4.0K Feb  1 13:41 .
pdns_1  | drwxr-xr-x 1 root root 4.0K Feb  2 15:12 ..
pdns_1  | -rw-r--r-- 1 root root 3.1K Feb  1 13:41 ixfrdist.example.yml
pdns_1  | -rw-r--r-- 1 root root  139 Feb  1 13:21 pdns.conf
pdns_1  | -rw-r--r-- 1 root root  18K Feb  1 13:41 pdns.conf-dist
pdns_1  | drwxr-xr-x 1 pdns pdns 4.0K Feb  2 15:12 pdns.d
pdns_1  | drwxr-xr-x 1 pdns pdns 4.0K Feb  1 21:32 templates.d
pdns_1  | Feb 02 15:12:51 Loading '/usr/local/lib/pdns/libgpgsqlbackend.so'
pdns_1  | Feb 02 15:12:51 This is a standalone pdns
pdns_1  | Feb 02 15:12:51 Listening on controlsocket in '/var/run/pdns/pdns.controlsocket'
pdns_1  | Feb 02 15:12:51 Fatal error: Trying to set unknown parameter 'gsqlite3-dnssec'

You can see the "temp" file was not created. Furthermore I do not think the templated file is being written out to pdns.conf since the script is ran as pdns but the file is owned by root. While the printing of the template shows that the jinja did work, the file was never written out.

This is why I think we are getting a fatal error when it is trying to set up an unknown parameter gsqlite3-dnssec. It is odd though the gpgsql backend gets loaded even though the file still remains the same as the default which loads sqlite.

Looking at the existing Dockerfile I think the issue can be fixed here:

https://github.com/PowerDNS/pdns/blob/c4602c32c1590051a62bfd6ec06d723855de281c/Dockerfile-auth#L84-L90

I think either change the permissions of just pdns.conf or change the ownership of /etc/powderdns/. Is there a reason why the user, pdns, does not own /etc/powderdns/? Seems like pdns owns some of the subdirectories in /etc/powerdns/ but not the whole directory.

james-crowley avatar Feb 02 '21 15:02 james-crowley

My mistake. I read the startup script code wrong and though the directory it was putting templates in was /etc/powderdns/ instead /etc/powerdns/pdns.d.

After looking in /etc/powerdns/pdns.d, I can see my templated file just fine all with the correct information. That explains why gpgsql loads up, but still leaves me confused as why gsqlite3-dnssec is throwing a fatal error. I assume it has to do with me switching the launch to launch=gpgsql, which makes only the gpgsql values valid.

I tried looking through the pdns documentation on the config files and what file takes precedence. The only information I am seeing is the documentation for include-dir. Am I missing something?

james-crowley avatar Feb 02 '21 18:02 james-crowley

Hi, I see this issue is still open but should maybe be closed and another opened if needed? I've just startet to verify pdns as a k8s deployment. Running pdns 4.6 in k8s, using readonlyfilesystems and the following works:

  1. mount rundirs as emptydir
  2. mount config.d dirs (pdns.d) as emptydirs
  3. template: pdns.j2
  4. start options: --config-dir=/etc/powerdns/pdns.d Same goes for recursor. I think db migration is mentioned in another issue and should be an option in startup, not in docker files (similar to what powerdns-admin does)

hwaastad avatar Apr 05 '22 07:04 hwaastad

Hi, I see this issue is still open but should maybe be closed and another opened if needed?

Why?

Habbie avatar Apr 05 '22 07:04 Habbie

Hi, I see this issue is still open but should maybe be closed and another opened if needed?

Why?

Hi, I was maybe a little trigger happy 😃 What I mean is that both auth and recursor is fully configurable through command options and j2 templates and env. And these can be mounted into docker or k8s in a separate mount (which is from a sec point if view a good thing. Then you can run the container with RO filesystem. To my understanding, the current docker is ok regarding the initial issue. But I’ve been wrong before 😃

Anyways, I think #10065 is a important one.

hwaastad avatar Apr 05 '22 19:04 hwaastad

#12176 seems related

Habbie avatar Nov 28 '22 14:11 Habbie

Is it possible to modify print("Created {} with content:\n{}\n".format(target, rendered)) to be printed only when envvar is set (i.e. if ENV_TEMPLATE_DEBUG is set - print what was created)? In current state it outputs generated template to stdout, and if this template contains passwords - that's not secure.

barzog avatar Dec 28 '22 10:12 barzog

Also got the error pdns_1 | Feb 02 15:12:51 Fatal error: Trying to set unknown parameter 'gsqlite3-dnssec' for while trying to use the MySQL backed in docker and using the ENV var to populate credentials.

Created a work around by overwriting the normal config with the backed I want to launch. pdns.conf

launch=gmysql
include-dir=/etc/powerdns/pdns.d

and include the generated config in the main.

This seems to work.

Now it won't use the default configuration and still apply the ENV vars.

aladante avatar Aug 08 '23 14:08 aladante

Here is a code example for the solution from @aladante. This will override the default config file and allows to use a postgres database as backend.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pdns-auth
  labels:
    app: pdns-auth
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pdns-auth
  template:
    metadata:
      labels:
        app: pdns-auth
    spec:
      containers:
      - name: pdns-auth
        image: powerdns/pdns-auth-49:4.9.0
        ports:
        # - containerPort: 53
        #   protocol: TCP
        - containerPort: 53
          protocol: UDP
        - containerPort: 8081
          protocol: TCP
        volumeMounts:
        - name: pdns-auth-config-volume
          mountPath: /etc/powerdns/pdns.conf
          subPath: pdns.conf
      volumes:
      - name: pdns-auth-config-volume
        configMap:
          name: pdns-auth-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: pdns-auth-config
data:
  pdns.conf: |
    launch=gpgsql
    gpgsql-host=pdns-postgres-db
    gpgsql-port=5432
    gpgsql-dbname=dns
    gpgsql-user=pdns
    gpgsql-password=

    local-port=53
    
    webserver=yes
    webserver-address=0.0.0.0
    webserver-allow-from=0.0.0.0/0
    webserver-port=8081
    webserver-password=secret
    
    api=yes
    api-key=secret

Tim-herbie avatar May 18 '24 15:05 Tim-herbie