This commit is contained in:
Matt Scott 2022-01-03 18:38:20 -07:00 committed by GitHub
commit ce175a5171
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1733 additions and 1644 deletions

View file

@ -97,6 +97,7 @@ npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
docker/.autoinit
# Git
.git

1
.gitignore vendored
View file

@ -26,6 +26,7 @@ nosetests.xml
flask
config.py
configs/production.py
docker/.autoinit
logfile.log
log.txt
pdns.db

View file

@ -1,9 +1,18 @@
# PowerDNS-Admin
A PowerDNS web interface with advanced features.
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:python)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:javascript)
The PowerDNS-Admin is a simple web GUI for managing zone configurations of a PowerDNS Authoritative server.
The PowerDNS-Admin app does NOT modify the PowerDNS Authoritative server database directly. Instead, it communicates with the PDNS server via the built-in HTTP API.
The app does have a database for identity management, access control, and caching which can be hosted in either MySQL or SQLite.
- [PowerDNS-Admin GitHub](https://github.com/ngoduykhanh/PowerDNS-Admin)
- [PowerDNS-Admin Settings](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/docs/settings.md)
- [PowerDNS-Admin Wiki](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki)
#### Features:
- Multiple domain management
- Domain template
@ -19,38 +28,16 @@ A PowerDNS web interface with advanced features.
- Limited API for manipulating zones and records
- Full IDN/Punycode support
## Running PowerDNS-Admin
There are several ways to run PowerDNS-Admin. The easiest way is to use Docker.
If you are looking to install and run PowerDNS-Admin directly onto your system check out the [Wiki](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki#installation-guides) for ways to do that.
## Deploying PowerDNS-Admin
There are multiple ways to run the PowerDNS-Admin app. The recommended method is to use the official [Docker images](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/docs/docker.md).
### Docker
This are two options to run PowerDNS-Admin using Docker.
To get started as quickly as possible try option 1. If you want to make modifications to the configuration option 2 may be cleaner.
If you would like to run PowerDNS-Admin directly on your machine or VM, check out the [Wiki](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki#installation-guides) for additional information.
#### Option 1: From Docker Hub
The easiest is to just run the latest Docker image from Docker Hub:
```
$ docker run -d \
-e SECRET_KEY='a-very-secret-key' \
-v pda-data:/data \
-p 9191:80 \
ngoduykhanh/powerdns-admin:latest
```
This creates a volume called `pda-data` to persist the SQLite database with the configuration.
Once you have deployed the app through one of the supported methods, You should be able to access the PowerDNS-Admin app by pointing your browser to http://localhost:8080.
#### Option 2: Using docker-compose
1. Update the configuration
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`.
Other environment variables are mentioned in the [legal_envvars](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/configs/docker_config.py#L5-L46).
To use the Docker secrets feature it is possible to append `_FILE` to the environment variables and point to a file with the values stored in it.
Make sure to set the environment variable `SECRET_KEY` to a long random string (https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY)
## Configuring PowerDNS-Admin
2. Start docker container
```
$ docker-compose up
```
You can then access PowerDNS-Admin by pointing your browser to http://localhost:9191.
The app has a plethora of settings that may be configured through a number of methods. Check out the settings documentation [here](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/docs/settings.md).
## Screenshots
![dashboard](https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png)

0
configs/default.py Normal file
View file

View file

@ -1,33 +1,35 @@
import os
#import urllib.parse
basedir = os.path.abspath(os.path.dirname(__file__))
### BASIC APP CONFIG
########################################
# Core App Config
########################################
PDA_BIND_ADDRESS = '0.0.0.0'
PDA_BIND_PORT = 80
PDA_DEBUG = True
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
SIGNUP_ENABLED = True
HSTS_ENABLED = False
OFFLINE_MODE = False
FILESYSTEM_SESSIONS_ENABLED = False
### DATABASE CONFIG
########################################
# MySQL Database Config
########################################
SQLA_DB_HOST = 'mysql'
SQLA_DB_PORT = 3306
SQLA_DB_USER = 'pda'
SQLA_DB_PASSWORD = 'changeme'
SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_PASSWORD = 'qHZDZ26fPqqCfKZtWkQ9'
SQLA_DB_NAME = 'pda'
SQLALCHEMY_TRACK_MODIFICATIONS = True
### DATABASE - MySQL
#SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
# urllib.parse.quote_plus(SQLA_DB_USER),
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
# SQLA_DB_HOST,
# SQLA_DB_NAME
#)
########################################
# SQLite Database Config
########################################
# Uncomment the following line to use SQLite instead of MySQL
# SQLALCHEMY_DATABASE_URI = 'sqlite:////srv/app/pdns.db'
### DATABASE - SQLite
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
### SMTP config
########################################
# SMTP Config
########################################
# MAIL_SERVER = 'localhost'
# MAIL_PORT = 25
# MAIL_DEBUG = False
@ -37,28 +39,27 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# MAIL_PASSWORD = None
# MAIL_DEFAULT_SENDER = ('PowerDNS-Admin', 'noreply@domain.ltd')
# SAML Authnetication
########################################
# SAML Authentication Config
########################################
SAML_ENABLED = False
# SAML_DEBUG = True
# SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
# ##Example for ADFS Metadata-URL
# ** Example for ADFS Metadata-URL
# SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
# #Cache Lifetime in Seconds
# ** Cache Lifetime in Seconds
# SAML_METADATA_CACHE_LIFETIME = 1
# # SAML SSO binding format to use
# ## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
# ** SAML SSO binding format to use
# *** Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
# #SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
# ## EntityID of the IdP to use. Only needed if more than one IdP is
# ## in the SAML_METADATA_URL
# ### Default: First (only) IdP in the SAML_METADATA_URL
# ### Example: https://idp.example.edu/idp
# #SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
# ## NameID format to request
# ### Default: The SAML NameID Format in the metadata if present,
# ### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
# ### Example: urn:oid:0.9.2342.19200300.100.1.1
# ** EntityID of the IdP to use. Only needed if more than one IdP is in the SAML_METADATA_URL
# *** Default: First (only) IdP in the SAML_METADATA_URL
# *** Example: https://idp.example.edu/idp
# SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
# ** NameID format to request
# *** Default: The SAML NameID Format in the metadata if present,
# *** otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
# *** Example: urn:oid:0.9.2342.19200300.100.1.1
# #SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
# Following parameter defines RequestedAttributes section in SAML metadata

View file

@ -1,115 +0,0 @@
# Defaults for Docker image
BIND_ADDRESS = '0.0.0.0'
PORT = 80
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
legal_envvars = (
'SECRET_KEY',
'OIDC_OAUTH_API_URL',
'OIDC_OAUTH_TOKEN_URL',
'OIDC_OAUTH_AUTHORIZE_URL',
'BIND_ADDRESS',
'PORT',
'LOG_LEVEL',
'SALT',
'SQLALCHEMY_TRACK_MODIFICATIONS',
'SQLALCHEMY_DATABASE_URI',
'MAIL_SERVER',
'MAIL_PORT',
'MAIL_DEBUG',
'MAIL_USE_TLS',
'MAIL_USE_SSL',
'MAIL_USERNAME',
'MAIL_PASSWORD',
'MAIL_DEFAULT_SENDER',
'SAML_ENABLED',
'SAML_DEBUG',
'SAML_PATH',
'SAML_METADATA_URL',
'SAML_METADATA_CACHE_LIFETIME',
'SAML_IDP_SSO_BINDING',
'SAML_IDP_ENTITY_ID',
'SAML_NAMEID_FORMAT',
'SAML_ATTRIBUTE_EMAIL',
'SAML_ATTRIBUTE_GIVENNAME',
'SAML_ATTRIBUTE_SURNAME',
'SAML_ATTRIBUTE_NAME',
'SAML_ATTRIBUTE_USERNAME',
'SAML_ATTRIBUTE_ADMIN',
'SAML_ATTRIBUTE_GROUP',
'SAML_GROUP_ADMIN_NAME',
'SAML_GROUP_TO_ACCOUNT_MAPPING',
'SAML_ATTRIBUTE_ACCOUNT',
'SAML_SP_ENTITY_ID',
'SAML_SP_CONTACT_NAME',
'SAML_SP_CONTACT_MAIL',
'SAML_SIGN_REQUEST',
'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT',
'SAML_LOGOUT_URL',
'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_LOGOUT_URL',
'REMOTE_USER_COOKIES',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'SAML_CERT',
'SAML_KEY',
'FILESYSTEM_SESSIONS_ENABLED'
)
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
legal_envvars_bool = (
'SQLALCHEMY_TRACK_MODIFICATIONS',
'HSTS_ENABLED',
'MAIL_DEBUG',
'MAIL_USE_TLS',
'MAIL_USE_SSL',
'SAML_ENABLED',
'SAML_DEBUG',
'SAML_SIGN_REQUEST',
'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT',
'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_ENABLED',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'FILESYSTEM_SESSIONS_ENABLED'
)
# import everything from environment variables
import os
import sys
def str2bool(v):
return v.lower() in ("true", "yes", "1")
for v in legal_envvars:
ret = None
# _FILE suffix will allow to read value from file, usefull for Docker's
# secrets feature
if v + '_FILE' in os.environ:
if v in os.environ:
raise AttributeError(
"Both {} and {} are set but are exclusive.".format(
v, v + '_FILE'))
with open(os.environ[v + '_FILE']) as f:
ret = f.read()
f.close()
elif v in os.environ:
ret = os.environ[v]
if ret is not None:
if v in legal_envvars_bool:
ret = str2bool(ret)
if v in legal_envvars_int:
ret = int(ret)
sys.modules[__name__].__dict__[v] = ret

View file

@ -1,34 +0,0 @@
version: "2.1"
services:
powerdns-admin:
build:
context: .
dockerfile: docker-test/Dockerfile
image: powerdns-admin-test
container_name: powerdns-admin-test
ports:
- "9191:80"
networks:
- default
env_file:
- ./docker-test/env
depends_on:
- pdns-server
pdns-server:
build:
context: .
dockerfile: docker-test/Dockerfile.pdns
image: pdns-server-test
ports:
- "5053:53"
- "5053:53/udp"
- "8081:8081"
networks:
- default
env_file:
- ./docker-test/env
networks:
default:

View file

@ -1,18 +0,0 @@
version: "3"
services:
app:
image: ngoduykhanh/powerdns-admin:latest
container_name: powerdns_admin
ports:
- "9191:80"
logging:
driver: json-file
options:
max-size: 50m
environment:
- SQLALCHEMY_DATABASE_URI=mysql://pda:changeme@host.docker.internal/pda
- GUNICORN_TIMEOUT=60
- GUNICORN_WORKERS=2
- GUNICORN_LOGLEVEL=DEBUG
- OFFLINE_MODE=False # True for offline, False for external resources

View file

@ -1,34 +0,0 @@
FROM debian:stretch-slim
LABEL maintainer="k@ndk.name"
ENV LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends apt-transport-https locales locales-all python3-pip python3-setuptools python3-dev curl libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev build-essential libmariadb-dev-compat \
&& curl -sL https://deb.nodesource.com/setup_10.x | bash - \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& apt-get update -y \
&& apt-get install -y nodejs yarn \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# We copy just the requirements.txt first to leverage Docker cache
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt
COPY . /app
COPY ./docker/entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENV FLASK_APP=powerdnsadmin/__init__.py
RUN yarn install --pure-lockfile --production \
&& yarn cache clean \
&& flask assets build
COPY ./docker-test/wait-for-pdns.sh /opt
RUN chmod u+x /opt/wait-for-pdns.sh
CMD ["/opt/wait-for-pdns.sh", "/usr/local/bin/pytest","--capture=no","-vv"]

View file

@ -1,13 +0,0 @@
FROM ubuntu:latest
RUN apt-get update && apt-get install -y pdns-backend-sqlite3 pdns-server sqlite3
COPY ./docker-test/pdns.sqlite.sql /data/pdns.sql
ADD ./docker-test/start.sh /data/
RUN rm -f /etc/powerdns/pdns.d/pdns.simplebind.conf
RUN rm -f /etc/powerdns/pdns.d/bind.conf
RUN chmod +x /data/start.sh && mkdir -p /var/empty/var/run
CMD /data/start.sh

View file

@ -1,5 +0,0 @@
PDNS_PROTO=http
PDNS_PORT=8081
PDNS_HOST=pdns-server
PDNS_API_KEY=changeme
PDNS_WEBSERVER_ALLOW_FROM=0.0.0.0/0

View file

@ -1,24 +0,0 @@
#!/usr/bin/env sh
if [ -z ${PDNS_API_KEY+x} ]; then
API_KEY=changeme
fi
if [ -z ${PDNS_PORT+x} ]; then
WEB_PORT=8081
fi
# Import schema structure
if [ -e "/data/pdns.sql" ]; then
rm /data/pdns.db
cat /data/pdns.sql | sqlite3 /data/pdns.db
rm /data/pdns.sql
echo "Imported schema structure"
fi
chown -R pdns:pdns /data/
/usr/sbin/pdns_server \
--launch=gsqlite3 --gsqlite3-database=/data/pdns.db \
--webserver=yes --webserver-address=0.0.0.0 --webserver-port=${PDNS_PORT} \
--api=yes --api-key=$PDNS_API_KEY --webserver-allow-from=${PDNS_WEBSERVER_ALLOW_FROM}

View file

@ -1,22 +0,0 @@
#!/bin/sh
set -e
CMD="$1"
shift
CMD_ARGS="$@"
LOOPS=10
until curl -H "X-API-Key: ${PDNS_API_KEY}" "${PDNS_PROTO}://${PDNS_HOST}:${PDNS_PORT}/api/v1/servers"; do
>&2 echo "PDNS is unavailable - sleeping"
sleep 1
if [ $LOOPS -eq 10 ]
then
break
fi
done
sleep 5
>&2 echo "PDNS is up - executing command"
exec $CMD $CMD_ARGS

View file

@ -1,95 +0,0 @@
FROM alpine:3.13 AS builder
LABEL maintainer="k@ndk.name"
ARG BUILD_DEPENDENCIES="build-base \
libffi-dev \
libxml2-dev \
mariadb-connector-c-dev \
openldap-dev \
python3-dev \
xmlsec-dev \
yarn \
cargo"
ENV LC_ALL=en_US.UTF-8 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US.UTF-8 \
FLASK_APP=/build/powerdnsadmin/__init__.py
# Get dependencies
# py3-pip should not belong to BUILD_DEPENDENCIES. Otherwise, when we remove
# them with "apk del" at the end of build stage, the python requests module
# will be removed as well - (Tested with alpine:3.12 and python 3.8.5).
RUN apk add --no-cache ${BUILD_DEPENDENCIES} && \
apk add --no-cache py3-pip
WORKDIR /build
# We copy just the requirements.txt first to leverage Docker cache
COPY ./requirements.txt /build/requirements.txt
# Get application dependencies
RUN pip install --upgrade pip && \
pip install -r requirements.txt
# Add sources
COPY . /build
# Prepare assets
RUN yarn install --pure-lockfile --production && \
yarn cache clean && \
sed -i -r -e "s|'cssmin',\s?'cssrewrite'|'cssmin'|g" /build/powerdnsadmin/assets.py && \
flask assets build
RUN mv /build/powerdnsadmin/static /tmp/static && \
mkdir /build/powerdnsadmin/static && \
cp -r /tmp/static/generated /build/powerdnsadmin/static && \
cp -r /tmp/static/assets /build/powerdnsadmin/static && \
cp -r /tmp/static/img /build/powerdnsadmin/static && \
find /tmp/static/node_modules -name 'fonts' -exec cp -r {} /build/powerdnsadmin/static \; && \
find /tmp/static/node_modules/icheck/skins/square -name '*.png' -exec cp {} /build/powerdnsadmin/static/generated \;
RUN { \
echo "from flask_assets import Environment"; \
echo "assets = Environment()"; \
echo "assets.register('js_login', 'generated/login.js')"; \
echo "assets.register('js_validation', 'generated/validation.js')"; \
echo "assets.register('css_login', 'generated/login.css')"; \
echo "assets.register('js_main', 'generated/main.js')"; \
echo "assets.register('css_main', 'generated/main.css')"; \
} > /build/powerdnsadmin/assets.py
# Move application
RUN mkdir -p /app && \
cp -r /build/migrations/ /build/powerdnsadmin/ /build/run.py /app && \
mkdir -p /app/configs && \
cp -r /build/configs/docker_config.py /app/configs
# Build image
FROM alpine:3.13
ENV FLASK_APP=/app/powerdnsadmin/__init__.py \
USER=pda
RUN apk add --no-cache mariadb-connector-c postgresql-client py3-gunicorn py3-psycopg2 xmlsec tzdata libcap && \
addgroup -S ${USER} && \
adduser -S -D -G ${USER} ${USER} && \
mkdir /data && \
chown ${USER}:${USER} /data && \
setcap cap_net_bind_service=+ep $(readlink -f /usr/bin/python3) && \
apk del libcap
COPY --from=builder /usr/bin/flask /usr/bin/
COPY --from=builder /usr/lib/python3.8/site-packages /usr/lib/python3.8/site-packages/
COPY --from=builder --chown=root:${USER} /app /app/
COPY ./docker/entrypoint.sh /usr/bin/
WORKDIR /app
RUN chown ${USER}:${USER} ./configs /app && \
cat ./powerdnsadmin/default_config.py ./configs/docker_config.py > ./powerdnsadmin/docker_config.py
EXPOSE 80/tcp
USER ${USER}
HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1/"]
ENTRYPOINT ["entrypoint.sh"]
CMD ["gunicorn","powerdnsadmin:create_app()"]

66
docker/alpine/Dockerfile Normal file
View file

@ -0,0 +1,66 @@
ARG DISTRO=alpine
ARG DISTRO_TAG=3.14
FROM ${DISTRO}:${DISTRO_TAG}
LABEL maintainer="matt@azorian.solutions"
# Set additional container environment variables
ENV LC_ALL=en_US.UTF-8 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US.UTF-8 \
PDA_UID=${PDA_UID:-1000} \
PDA_USER=${PDA_USER:-pda} \
PDA_GID=${PDA_GID:-1000} \
PDA_GROUP=${PDA_GROUP:-pda} \
FLASK_APP=${FLASK_APP:-/srv/app/run.py}
# Perform the following tasks:
# - Install required APK packages per the required dependencies
# - Upgrade Python pip package using pip3
RUN apk update \
&& apk add --no-cache bash grep curl build-base tzdata libcap libffi-dev libxml2-dev xmlsec-dev mariadb-connector-c-dev \
openldap-dev python3-dev py3-pip py3-gunicorn py3-psycopg2 yarn cargo mysql-client postgresql-client sqlite \
&& pip install --upgrade pip
# Copy Python pip requirements file for early installation which leverages the image caching mechanisms
COPY /requirements.txt /srv/app/requirements.txt
# Change the working directory which may affect the build contexts of following commands
WORKDIR /srv/app
# Install the required Python pip packages as defined in the ./requirements.txt file
RUN pip install -r requirements.txt
# Copy all project files into the container's /app directory
COPY . /srv/app
# Perform the following tasks:
# - Create system group and user as configured in the PDA_GROUP and PDA_USER environment variables
# - Copy additional automation scripts to their appropriate locations
# - Set permissions on automation scripts
# - Install Yarn and clean the Yarn cache
# - Build Flask assets
# - Apply netcaps and cleanup APK packages
RUN addgroup -S --gid $PDA_GID $PDA_GROUP \
&& adduser -S -D --uid $PDA_UID -G $PDA_GROUP $PDA_USER \
# XXX: Is this setcap still needed?
&& setcap cap_net_bind_service=+ep $(readlink -f /usr/bin/python3) \
&& apk del libcap \
&& yarn install --pure-lockfile --production \
&& yarn cache clean \
&& flask assets build
# Set the system user and group for the primary container process
USER "${PDA_USER}:${PDA_GROUP}"
# Define the app directory as a volume
VOLUME /srv/app
# Expose the configured service port
EXPOSE 80/TCP
# Set the entrypoint script of the container
ENTRYPOINT ["/bin/bash", "/srv/app/docker/shared/entrypoint"]
# Set the default command of the container to be ran following the entrypoint script
CMD ["/bin/sh", "-c", "/srv/app/run.py"]

49
docker/bin/build-image Executable file
View file

@ -0,0 +1,49 @@
#!/bin/bash
script_path=$(readlink -f "$0")
bin_path=$(dirname "$script_path")
app_path=$(dirname $(dirname "$bin_path"))
docker_path=$app_path/docker
docker_repo_config_path=$docker_path/repo.cfg
docker_distro=alpine
docker_distro_tag=3.14
docker_tag=latest
if [ ! -z "$1" ];
then
docker_distro=$1
fi
if [ ! -z "$2" ];
then
docker_distro_tag=$2
fi
if [ ! -z "$3" ];
then
docker_tag=$3
fi
if [ -f "$docker_repo_config_path" ];
then
docker_repo=`cat $docker_repo_config_path`
if [ -f "$docker_path/$docker_distro/Dockerfile-$docker_tag" ];
then
docker_file=$docker_path/$docker_distro/Dockerfile-$docker_tag
elif [ -f "$docker_path/$docker_distro/Dockerfile-$docker_distro-$docker_distro_tag" ];
then
docker_file=$docker_path/$docker_distro/Dockerfile-$docker_distro-$docker_distro_tag
elif [ -f "$docker_path/$docker_distro/Dockerfile-$docker_distro" ];
then
docker_file=$docker_path/$docker_distro/Dockerfile-$docker_distro
else
docker_file=$docker_path/$docker_distro/Dockerfile
fi
docker build --force-rm -f $docker_file -t $docker_repo:$docker_tag --build-arg DISTRO=$docker_distro --build-arg DISTRO_TAG=$docker_distro_tag $app_path
else
echo "Could not load Docker registry path from $docker_repo_config_path"
exit 1
fi

View file

@ -0,0 +1 @@
#!/bin/bash

77
docker/debian/Dockerfile Normal file
View file

@ -0,0 +1,77 @@
ARG DISTRO=debian
ARG DISTRO_TAG=11.1-slim
FROM ${DISTRO}:${DISTRO_TAG}
LABEL maintainer="matt@azorian.solutions"
# Set additional container environment variables
ENV LC_ALL=en_US.UTF-8 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US.UTF-8 \
PDA_UID=${PDA_UID:-1000} \
PDA_USER=${PDA_USER:-pda} \
PDA_GID=${PDA_GID:-1000} \
PDA_GROUP=${PDA_GROUP:-pda} \
FLASK_APP=${FLASK_APP:-/srv/app/run.py}
# Perform the following tasks:
# - Install required APT packages per the required dependencies
# - Download and execute NodeSource setup package
# - Download and add Yarn public GPG key to APT
# - Add Yarn repository to the APT configuration
# - Update APT cache
# - Install NodeJS and Yarn APT packages
# - Clean APT caches and dynamic lists
# - Upgrade Python pip package using pip3
RUN apt update -y \
&& apt install -y --no-install-recommends apt-transport-https locales locales-all python3-pip python3-setuptools \
python3-dev curl libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxmlsec1 libxslt1-dev libxmlsec1-dev xmlsec1 \
libffi-dev build-essential libmariadb-dev-compat pkg-config default-mysql-client postgresql-client sqlite3 \
netcat-openbsd \
&& curl -sL https://deb.nodesource.com/setup_14.x | bash - \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& apt update -y \
&& apt install -y nodejs yarn \
&& apt clean -y \
&& rm -rf /var/lib/apt/lists/* \
&& pip3 install --upgrade pip
# Copy Python pip requirements file for early installation which leverages the image caching mechanisms
COPY /requirements.txt /srv/app/requirements.txt
# Change the working directory which may affect the build contexts of following commands
WORKDIR /srv/app
# Install the required Python pip packages as defined in the ./requirements.txt file
RUN pip3 install -r requirements.txt
# Copy all project files into the container's /srv/app directory
COPY . /srv/app
# Perform the following tasks:
# - Copy additional automation scripts to their appropriate locations
# - Set permissions on automation scripts
# - Install Yarn and clean the Yarn cache
# - Build Flask assets
RUN addgroup --system --gid $PDA_GID $PDA_GROUP \
&& adduser --system --disabled-login --no-create-home --home /tmp --shell /bin/false --uid $PDA_UID \
--ingroup $PDA_GROUP $PDA_USER 2>/dev/null \
&& yarn install --pure-lockfile --production \
&& yarn cache clean \
&& flask assets build
# Set the system user and group for the primary container process
USER "${PDA_USER}:${PDA_GROUP}"
# Define the app directory as a volume
VOLUME /srv/app
# Expose the configured service port
EXPOSE 80/TCP
# Set the entrypoint script of the container
ENTRYPOINT ["/bin/bash", "/srv/app/docker/shared/entrypoint"]
# Set the default command of the container to be ran following the entrypoint script
CMD ["/bin/sh", "-c", "/srv/app/run.py"]

View file

@ -1,17 +0,0 @@
#!/bin/sh
set -euo pipefail
cd /app
GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-120}"
GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}"
GUNICORN_LOGLEVEL="${GUNICORN_LOGLEVEL:-info}"
BIND_ADDRESS="${BIND_ADDRESS:-0.0.0.0:80}"
GUNICORN_ARGS="-t ${GUNICORN_TIMEOUT} --workers ${GUNICORN_WORKERS} --bind ${BIND_ADDRESS} --log-level ${GUNICORN_LOGLEVEL}"
if [ "$1" == gunicorn ]; then
/bin/sh -c "flask db upgrade"
exec "$@" $GUNICORN_ARGS
else
exec "$@"
fi

1
docker/repo.cfg Normal file
View file

@ -0,0 +1 @@
ngoduykhanh/powerdns-admin

238
docker/shared/entrypoint Executable file
View file

@ -0,0 +1,238 @@
#!/bin/bash
script_path=$(readlink -f "$0")
app_path=$(dirname $(dirname $(dirname "$script_path")))
docker_path=$app_path/docker
auto_init_path=$docker_path/.autoinit
########################################################################################################################
# START CONFIGURATION
########################################################################################################################
# The container interface IP mask to bind both gunicorn and the built-in web server to
PDA_BIND_ADDRESS=${PDA_BIND_ADDRESS:-0.0.0.0}
# The container port to bind both gunicorn and the built-in web server to
PDA_BIND_PORT=${PDA_BIND_PORT:-80}
# The debug mode setting of the app
PDA_DEBUG=${PDA_DEBUG:-false}
# Whether to automatically convert environment variables beginning with PDA_ and ending with _FILE
PDA_LOAD_ENV_FILES=${PDA_LOAD_ENV_FILES:-true}
# Whether the automatic first-time initialization feature is enabled
PDA_AUTOINIT_ENABLED=${PDA_AUTOINIT_ENABLED:-false}
# Whether to force execution of the first-time initialization feature
PDA_AUTOINIT_FORCE=${PDA_AUTOINIT_FORCE:-false}
# Whether to delay container startup until the configured MySQL server is online
PDA_CHECK_MYSQL_ENABLED=${PDA_CHECK_MYSQL_ENABLED:-false}
# How many seconds to wait after a MySQL connection attempt failure before trying again
PDA_CHECK_MYSQL_FAIL_DELAY=${PDA_CHECK_MYSQL_FAIL_DELAY:-2}
# How many seconds to wait after a successful MySQL connection attempt before proceeding to the next step
PDA_CHECK_MYSQL_SUCCESS_DELAY=${PDA_CHECK_MYSQL_SUCCESS_DELAY:-0}
# How many MySQL connection attempts should be made before halting container execution
PDA_CHECK_MYSQL_ATTEMPTS=${PDA_CHECK_MYSQL_ATTEMPTS:-30}
# Whether to delay container startup until the configured PDNS API key passes authorization to the server
PDA_CHECK_API_ENABLED=${PDA_CHECK_API_ENABLED:-false}
# How many seconds to wait after an API connection attempt failure before trying again
PDA_CHECK_API_FAIL_DELAY=${PDA_CHECK_API_FAIL_DELAY:-2}
# How many seconds to wait after an API successful connection attempt before proceeding to the next step
PDA_CHECK_API_SUCCESS_DELAY=${PDA_CHECK_API_SUCCESS_DELAY:-0}
# How many API connection attempts should be made before halting container execution
PDA_CHECK_API_ATTEMPTS=${PDA_CHECK_API_ATTEMPTS:-15}
# Whether to run the app's Python test unit before starting the primary container process
PDA_CHECK_PYTEST_ENABLED=${PDA_CHECK_PYTEST_ENABLED:-false}
# Whether to enable the use of gunicorn
PDA_GUNICORN_ENABLED=${PDA_GUNICORN_ENABLED:-false}
# The request timeout in seconds for a gunicorn worker
PDA_GUNICORN_TIMEOUT=${PDA_GUNICORN_TIMEOUT:-120}
# The total number of gunicorn worker threads to spawn
PDA_GUNICORN_WORKERS=${PDA_GUNICORN_WORKERS:-4}
# The log output level of the gunicorn process
PDA_GUNICORN_LOGLEVEL=${PDA_GUNICORN_LOGLEVEL:-info}
# The additional gunicorn arguments that will be appended to the startup command when gunicorn is enabled
PDA_GUNICORN_ARGS=${PDA_GUNICORN_ARGS:-"-t ${PDA_GUNICORN_TIMEOUT} -w ${PDA_GUNICORN_WORKERS} \
--log-level ${PDA_GUNICORN_LOGLEVEL} --bind ${PDA_BIND_ADDRESS}:${PDA_BIND_PORT}"}
# The API key for the associated PowerDNS Authoritative server API
PDA_PDNS_API_KEY=${PDA_PDNS_API_KEY:-CHANGE_ME!}
# The API version of the associated PowerDNS Authoritative server API
PDA_PDNS_API_VERSION=${PDA_PDNS_API_VERSION:-4.5.2}
# The HTTP protocol for the associated PowerDNS Authoritative server API
PDA_PDNS_API_PROTO=${PDA_PDNS_API_PROTO:-http}
# The hostname or IP address of the associated PowerDNS Authoritative server API
PDA_PDNS_API_HOST=${PDA_PDNS_API_HOST:-127.0.0.1}
# The port of the associated PowerDNS Authoritative server API
PDA_PDNS_API_PORT=${PDA_PDNS_API_PORT:-8081}
# The username to use for automatic admin account setup on startup
PDA_ADMIN_USER_USERNAME=${PDA_ADMIN_USER_USERNAME:-}
# The password to use for automatic admin account setup on startup
PDA_ADMIN_USER_PASSWORD=${PDA_ADMIN_USER_PASSWORD:-}
########################################################################################################################
# END CONFIGURATION
########################################################################################################################
convert_file_vars() {
for line in $(env); do
len=$(($(echo $line | wc -c) - 1))
key=$(echo $line | cut -c1-$(echo $line | grep -aob '=' | grep -oE '[0-9]+'))
value=$(echo $line | cut -c$(($(echo $line | grep -aob '=' | grep -oE '[0-9]+') + 2))-$len)
if [[ $key == PDA_* && $key == *_FILE && -n "$value" && -f "$value" ]]; then
len=$(($(echo $key | wc -c) - 1))
new_key=${key:0:$((len - 5))}
file_value=$(cat "$value")
export "$new_key"="$file_value"
unset "$key"
fi
done
}
auto_init() {
# Determine if the first run file exists and abort if so
if ([ ! -n "$PDA_AUTOINIT_FORCE" ] || [ ! "$PDA_AUTOINIT_FORCE" = "true" ]) && [ -f "$auto_init_path" ]; then
echo "Automatic initialization will abort as a first run file exists at $auto_init_path"
return
fi
# Assume MySQL database if the SQLA_DB_HOST environment variable has been set, otherwise assume SQLite
if [ -n "$PDAC_SQLA_DB_HOST" ] && [ -n "$PDAC_SQLA_DB_USER" ] && [ -n "$PDAC_SQLA_DB_PASSWORD" ] \
&& [ -n "$PDAC_SQLA_DB_PASSWORD" ] && [ -n "$PDAC_SQLA_DB_NAME" ]; then
table=tsigkeys
sql=$(printf "SHOW TABLES LIKE '%s'" "$table")
if [[ -z $(mysql -h $PDAC_SQLA_DB_HOST -u $PDAC_SQLA_DB_USER -p$PDAC_SQLA_DB_PASSWORD -e "$sql" $PDAC_SQLA_DB_NAME) ]]; then
echo "The table $table does not exist so the base pdns schema will be installed."
mysql_result=$(mysql -h $PDAC_SQLA_DB_HOST -u $PDAC_SQLA_DB_USER -p$PDAC_SQLA_DB_PASSWORD $PDAC_SQLA_DB_NAME < /srv/app/docker/shared/pdns-schema-mysql.sql)
else
echo "The table $table does exist so no further action will be taken."
fi
fi
if [ ! -f "/srv/app/pdns.db" ]; then
cat /srv/app/docker/shared/pdns-schema-sqlite.sql | sqlite3 /srv/app/pdns.db
fi
touch $auto_init_path;
}
verify_mysql_ready() {
local host=$1
local port=$2
local retry_executed=1
while ! nc -z $host $port >& /dev/null; do
# The last connection test to the MySQL server failed at this point
# If the remaining retry counter falls to zero, exit the connection test cycle
if [ $retry_executed -ge $PDA_CHECK_MYSQL_ATTEMPTS ]; then
echo "The maximum number ($PDA_CHECK_MYSQL_ATTEMPTS) of TCP connection tests have been executed without success. This container will now exit."
exit 1
else
echo "MySQL server is offline. The TCP connection test cycle number $retry_executed has failed to $host:$port. Waiting for $PDA_CHECK_MYSQL_FAIL_DELAY seconds..."
fi
# Delay execution for the configured MySQL fail delay
sleep $PDA_CHECK_MYSQL_FAIL_DELAY
# Increment the retry execution counter
retry_executed=$((retry_executed + 1))
done
echo "MySQL server is online after $retry_executed check(s). Delaying execution for $PDA_CHECK_MYSQL_SUCCESS_DELAY seconds..."
sleep $PDA_CHECK_MYSQL_SUCCESS_DELAY
}
verify_api_ready() {
local retry_executed=1
local api_uri="$PDA_PDNS_PROTO://$PDA_PDNS_HOST:$PDA_PDNS_PORT/api/v1/servers"
local auth_header="X-API-Key: $PDA_PDNS_API_KEY"
while [ $(curl -s -o /dev/null -w "%{http_code}" -H "$auth_header" "$api_uri") -ne "200" ]; do
# The last connection test to the API server failed at this point
# If the remaining retry counter falls to zero, exit the connection test cycle
if [ $retry_executed -ge $PDA_CHECK_API_ATTEMPTS ]; then
echo "The maximum number ($PDA_CHECK_API_ATTEMPTS) of API server HTTP connection tests have been executed without success. This container will now exit."
exit 1
else
echo "PowerDNS API server is offline. The HTTP connection test cycle number $retry_executed has failed to $api_uri. Waiting for $PDA_CHECK_API_FAIL_DELAY seconds..."
fi
# Delay execution for the configured API fail delay
sleep $PDA_CHECK_API_FAIL_DELAY
# Increment the retry execution counter
retry_executed=$((retry_executed + 1))
done
echo "PowerDNS API server is online after $retry_executed check(s). Delaying execution for ${PDA_CHECK_API_SUCCESS_DELAY} seconds..."
sleep $PDA_CHECK_API_SUCCESS_DELAY
}
# Automatically load any environment variables that begin with "PDA_" and end with "_FILE" if this feature is enabled
if [ "$PDA_LOAD_ENV_FILES" = "true" ]; then
convert_file_vars
fi
# Verify that the configured MySQL server is ready for connections
if [ "$PDA_CHECK_MYSQL_ENABLED" = "true" ] && [ -n "$PDAC_SQLA_DB_HOST" ]; then
verify_mysql_ready "$PDAC_SQLA_DB_HOST" "${PDAC_SQLA_DB_PORT:-3306}"
fi
# Execute first-run automatic initialization
# - Setup the base PowerDNS Authoritative server database schema if this is the first run if this feature is enabled
if [ "$PDA_AUTOINIT_ENABLED" = "true" ] || [ "$PDA_AUTOINIT_FORCE" = "true" ]; then
auto_init
fi
# Verify that the configured PowerDNS name server API is ready for connections if this feature is enabled
if [ "$PDA_CHECK_API_ENABLED" = "true" ] && [ -n "$PDA_PDNS_HOST" ]; then
verify_api_ready
fi
echo "Proceeding with app initialization..."
cd /srv/app
# Execute any pending database upgrades via Flask
flask db upgrade
if [ "$FLASK_ENV" = "development" ]; then
flask assets build
fi
# Run Python test units if this feature is enabled
if [ "$PDA_CHECK_PYTEST_ENABLED" = "true" ]; then
pytest
fi
# If gunicorn is configured for use then execute the gunicorn command with the configuration arguments and container
# command appended. Otherwise, just execute the container command.
if [ "$PDA_GUNICORN_ENABLED" = "true" ]; then
exec "gunicorn" $PDA_GUNICORN_ARGS "$@"
else
exec "$@"
fi

View file

@ -0,0 +1,89 @@
CREATE TABLE domains (
id INT AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
master VARCHAR(128) DEFAULT NULL,
last_check INT DEFAULT NULL,
type VARCHAR(6) NOT NULL,
notified_serial INT UNSIGNED DEFAULT NULL,
account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE UNIQUE INDEX name_index ON domains(name);
CREATE TABLE records (
id BIGINT AUTO_INCREMENT,
domain_id INT DEFAULT NULL,
name VARCHAR(255) DEFAULT NULL,
type VARCHAR(10) DEFAULT NULL,
content VARCHAR(64000) DEFAULT NULL,
ttl INT DEFAULT NULL,
prio INT DEFAULT NULL,
disabled TINYINT(1) DEFAULT 0,
ordername VARCHAR(255) BINARY DEFAULT NULL,
auth TINYINT(1) DEFAULT 1,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
CREATE INDEX ordername ON records (ordername);
CREATE TABLE supermasters (
ip VARCHAR(64) NOT NULL,
nameserver VARCHAR(255) NOT NULL,
account VARCHAR(40) CHARACTER SET 'utf8' NOT NULL,
PRIMARY KEY (ip, nameserver)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE TABLE comments (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(10) NOT NULL,
modified_at INT NOT NULL,
account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
comment TEXT CHARACTER SET 'utf8' NOT NULL,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX comments_name_type_idx ON comments (name, type);
CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);
CREATE TABLE domainmetadata (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
kind VARCHAR(32),
content TEXT,
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX domainmetadata_idx ON domainmetadata (domain_id, kind);
CREATE TABLE cryptokeys (
id INT AUTO_INCREMENT,
domain_id INT NOT NULL,
flags INT NOT NULL,
active BOOL,
published BOOL DEFAULT 1,
content TEXT,
PRIMARY KEY(id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE INDEX domainidindex ON cryptokeys(domain_id);
CREATE TABLE tsigkeys (
id INT AUTO_INCREMENT,
name VARCHAR(255),
algorithm VARCHAR(50),
secret VARCHAR(255),
PRIMARY KEY (id)
) Engine=InnoDB CHARACTER SET 'latin1';
CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);

View file

@ -89,4 +89,4 @@ CREATE TABLE tsigkeys (
secret VARCHAR(255)
);
CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);
CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);

View file

@ -0,0 +1,104 @@
version: "3.3"
services:
mysql:
image: mariadb:10.6
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
- MYSQL_DATABASE=pda
- MYSQL_USER=pda
- MYSQL_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
phpmyadmin:
image: phpmyadmin/phpmyadmin:5.1
depends_on: ['mysql']
restart: unless-stopped
environment:
- PMA_HOST=mysql
- PMA_PORT=3306
ports:
- 8082:80
powerdns-admin:
image: ngoduykhanh/powerdns-admin:latest
depends_on: [ 'mysql' ]
restart: unless-stopped
environment:
- PDA_BIND_ADDRESS=0.0.0.0
- PDA_BIND_PORT=80
- PDA_DEBUG=true
- PDA_LOAD_ENV_FILES=true
- PDA_AUTOINIT_ENABLED=true
- PDA_AUTOINIT_FORCE=false
- PDA_CHECK_MYSQL_ENABLED=true
- PDA_CHECK_MYSQL_FAIL_DELAY=2
- PDA_CHECK_MYSQL_SUCCESS_DELAY=0
- PDA_CHECK_MYSQL_ATTEMPTS=30
- PDA_CHECK_API_ENABLED=true
- PDA_CHECK_API_FAIL_DELAY=2
- PDA_CHECK_API_SUCCESS_DELAY=0
- PDA_CHECK_API_ATTEMPTS=15
- PDA_CHECK_PYTEST_ENABLED=false
- PDA_GUNICORN_ENABLED=false
- PDA_GUNICORN_TIMEOUT=2
- PDA_GUNICORN_WORKERS=2
- PDA_GUNICORN_LOGLEVEL=debug
- PDA_PDNS_API_KEY=scjfNRN4Ch2DBuhk24vr
- PDA_PDNS_VERSION=4.5.2
- PDA_PDNS_PROTO=http
- PDA_PDNS_HOST=pdns
- PDA_PDNS_PORT=8081
- PDA_ADMIN_USER_USERNAME=nocadmin
- PDA_ADMIN_USER_PASSWORD=clink henchmen mete coracle uphold
- PDAC_SALT=ntR8BmMQb7MKTJ5YrgQwZr3pxU3uLG
- PDAC_SECRET_KEY=eJ92QLuennVStHTj5FVwxz7m4r3Ng3
- PDAC_SIGNUP_ENABLED=true
- PDAC_HSTS_ENABLED=false
- PDAC_OFFLINE_MODE=false
- PDAC_FILESYSTEM_SESSIONS_ENABLED=false
- PDAC_SQLA_DB_HOST=mysql
- PDAC_SQLA_DB_NAME=pda
- PDAC_SQLA_DB_USER=root
- PDAC_SQLA_DB_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
- PDAC_SQLA_DB_PORT=3306
- PDAC_SAML_ENABLED=false
- SQLALCHEMY_TRACK_MODIFICATIONS=true
- FLASK_ENV=development
ports:
- "8080:80"
volumes:
- type: bind
source: ${PDA_LOCAL_PATH}
target: /srv/app
pdns:
image: azoriansolutions/powerdns-nameserver:4.5.2-alpine-3.14-mysql
depends_on: ['powerdns-admin']
restart: unless-stopped
environment:
- AS_MYSQL_CHECK_RETRY=10
- AS_MYSQL_CHECK_INTERVAL=4
- PDNS_launch=gmysql
- PDNS_gmysql_host=mysql
- PDNS_gmysql_port=3306
- PDNS_gmysql_user=root
- PDNS_gmysql_password=qHZDZ26fPqqCfKZtWkQ9
- PDNS_gmysql_dbname=pda
- PDNS_gmysql_dnssec=yes
- PDNS_default_soa_content=pdns noc.@ 0 10800 3600 604800 3600
- PDNS_default_ttl=3600
- PDNS_local_address=0.0.0.0
- PDNS_local_port=53
- PDNS_api=yes
- PDNS_api_key=scjfNRN4Ch2DBuhk24vr
- PDNS_webserver=yes
- PDNS_webserver_address=0.0.0.0
- PDNS_webserver_allow_from=0.0.0.0/0
- PDNS_any_to_tcp=yes
- PDNS_master=no
ports:
- "5053:53/tcp"
- "5053:53/udp"
- "8081:8081/tcp"

View file

@ -0,0 +1,96 @@
version: "3.3"
services:
mysql:
image: mariadb:10.6
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
- MYSQL_DATABASE=pda
- MYSQL_USER=pda
- MYSQL_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
phpmyadmin:
image: phpmyadmin/phpmyadmin:5.1
depends_on: ['mysql']
restart: unless-stopped
environment:
- PMA_HOST=mysql
- PMA_PORT=3306
ports:
- 8082:80
powerdns-admin:
image: ngoduykhanh/powerdns-admin:latest
depends_on: [ 'mysql' ]
restart: unless-stopped
environment:
- PDA_BIND_ADDRESS=0.0.0.0
- PDA_BIND_PORT=80
- PDA_DEBUG=true
- PDA_LOAD_ENV_FILES=true
- PDA_AUTOINIT_ENABLED=true
- PDA_AUTOINIT_FORCE=false
- PDA_CHECK_API_ENABLED=true
- PDA_CHECK_API_FAIL_DELAY=2
- PDA_CHECK_API_SUCCESS_DELAY=0
- PDA_CHECK_API_ATTEMPTS=15
- PDA_CHECK_PYTEST_ENABLED=false
- PDA_GUNICORN_ENABLED=false
- PDA_GUNICORN_TIMEOUT=2
- PDA_GUNICORN_WORKERS=2
- PDA_GUNICORN_LOGLEVEL=debug
- PDA_PDNS_API_KEY=scjfNRN4Ch2DBuhk24vr
- PDA_PDNS_VERSION=4.5.2
- PDA_PDNS_PROTO=http
- PDA_PDNS_HOST=pdns
- PDA_PDNS_PORT=8081
- PDA_ADMIN_USER_USERNAME=nocadmin
- PDA_ADMIN_USER_PASSWORD=clink henchmen mete coracle uphold
- PDAC_SALT=ntR8BmMQb7MKTJ5YrgQwZr3pxU3uLG
- PDAC_SECRET_KEY=eJ92QLuennVStHTj5FVwxz7m4r3Ng3
- PDAC_SIGNUP_ENABLED=true
- PDAC_HSTS_ENABLED=false
- PDAC_OFFLINE_MODE=false
- PDAC_FILESYSTEM_SESSIONS_ENABLED=false
- PDAC_SQLALCHEMY_DATABASE_URI=sqlite:////srv/app/pdns.db
- PDAC_SAML_ENABLED=false
- SQLALCHEMY_TRACK_MODIFICATIONS=true
- FLASK_ENV=development
ports:
- "8080:80"
volumes:
- type: bind
source: ${PDA_LOCAL_PATH}
target: /srv/app
pdns:
image: azoriansolutions/powerdns-nameserver:4.5.2-alpine-3.14-mysql
depends_on: ['powerdns-admin']
restart: unless-stopped
environment:
- AS_MYSQL_CHECK_RETRY=10
- AS_MYSQL_CHECK_INTERVAL=4
- PDNS_launch=gmysql
- PDNS_gmysql_host=mysql
- PDNS_gmysql_port=3306
- PDNS_gmysql_user=root
- PDNS_gmysql_password=qHZDZ26fPqqCfKZtWkQ9
- PDNS_gmysql_dbname=pda
- PDNS_gmysql_dnssec=yes
- PDNS_default_soa_content=pdns noc.@ 0 10800 3600 604800 3600
- PDNS_default_ttl=3600
- PDNS_local_address=0.0.0.0
- PDNS_local_port=53
- PDNS_api=yes
- PDNS_api_key=scjfNRN4Ch2DBuhk24vr
- PDNS_webserver=yes
- PDNS_webserver_address=0.0.0.0
- PDNS_webserver_allow_from=0.0.0.0/0
- PDNS_any_to_tcp=yes
- PDNS_master=no
ports:
- "5053:53/tcp"
- "5053:53/udp"
- "8081:8081/tcp"

View file

@ -0,0 +1,40 @@
version: "3.3"
services:
mysql:
image: mariadb:10.6
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
- MYSQL_DATABASE=pda
- MYSQL_USER=pda
- MYSQL_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
powerdns-admin:
image: ngoduykhanh/powerdns-admin:0.2.4
depends_on: ['mysql']
restart: unless-stopped
environment:
- PDA_CHECK_MYSQL_ENABLED=true
- PDA_CHECK_API_ENABLED=true
- PDA_GUNICORN_ENABLED=true
- PDA_GUNICORN_TIMEOUT=2
- PDA_GUNICORN_WORKERS=10
- PDA_GUNICORN_LOGLEVEL=info
- PDA_PDNS_API_KEY=YOUR-PDNS-API-SERVER-API-KEY
- PDA_PDNS_VERSION=YOUR-PDNS-API-SERVER-VERSION
- PDA_PDNS_PROTO=YOUR-PDNS-API-SERVER-HTTP-PROTOCOL
- PDA_PDNS_HOST=YOUR-PDNS-API-SERVER-HOSTNAME
- PDA_PDNS_PORT=YOUR-PDNS-API-SERVER-PORT
- PDAC_SALT=ntR8BmMQb7MKTJ5YrgQwZr3pxU3uLG
- PDAC_SECRET_KEY=eJ92QLuennVStHTj5FVwxz7m4r3Ng3
- PDAC_SIGNUP_ENABLED=false
- PDAC_HSTS_ENABLED=true
- PDAC_SQLA_DB_HOST=mysql
- PDAC_SQLA_DB_NAME=pda
- PDAC_SQLA_DB_USER=root
- PDAC_SQLA_DB_PASSWORD=qHZDZ26fPqqCfKZtWkQ9
- PDAC_SQLA_DB_PORT=3306
- PDAC_SAML_ENABLED=false
- SQLALCHEMY_TRACK_MODIFICATIONS=false
- FLASK_ENV=production
ports:
- "8080:80"

View file

@ -0,0 +1,26 @@
version: "3.3"
services:
powerdns-admin:
image: ngoduykhanh/powerdns-admin:0.2.4
restart: unless-stopped
environment:
- PDA_CHECK_API_ENABLED=true
- PDA_GUNICORN_ENABLED=true
- PDA_GUNICORN_TIMEOUT=2
- PDA_GUNICORN_WORKERS=10
- PDA_GUNICORN_LOGLEVEL=info
- PDA_PDNS_API_KEY=YOUR-PDNS-API-SERVER-API-KEY
- PDA_PDNS_VERSION=YOUR-PDNS-API-SERVER-VERSION
- PDA_PDNS_PROTO=YOUR-PDNS-API-SERVER-HTTP-PROTOCOL
- PDA_PDNS_HOST=YOUR-PDNS-API-SERVER-HOSTNAME
- PDA_PDNS_PORT=YOUR-PDNS-API-SERVER-PORT
- PDAC_SALT=ntR8BmMQb7MKTJ5YrgQwZr3pxU3uLG
- PDAC_SECRET_KEY=eJ92QLuennVStHTj5FVwxz7m4r3Ng3
- PDAC_SIGNUP_ENABLED=false
- PDAC_HSTS_ENABLED=true
- PDAC_SQLALCHEMY_DATABASE_URI=sqlite:////srv/app/pdns.db
- PDAC_SAML_ENABLED=false
- SQLALCHEMY_TRACK_MODIFICATIONS=false
- FLASK_ENV=production
ports:
- "8080:80"

142
docs/docker.md Normal file
View file

@ -0,0 +1,142 @@
# PowerDNS-Admin packaged by [Azorian Solutions](https://azorian.solutions)
The PowerDNS-Admin is a simple web GUI for managing zone configurations of a PowerDNS Authoritative server.
The PowerDNS-Admin app does NOT modify the PowerDNS Authoritative server database directly. Instead, it communicates with the PDNS server via the built-in HTTP API.
The app does have a database for identity management, access control, and caching which can be hosted in either MySQL or SQLite.
- [PowerDNS-Admin GitHub](https://github.com/ngoduykhanh/PowerDNS-Admin)
- [PowerDNS-Admin Settings](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/docs/settings.md)
- [PowerDNS-Admin Wiki](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki)
## Quick reference
- **Maintained by:** [Matt Scott](https://github.com/AzorianSolutions)
- **Github:** [https://github.com/AzorianSolutions](https://github.com/AzorianSolutions)
- **Website:** [https://azorian.solutions](https://azorian.solutions)
## TL;DR
docker run -d -p 8080:80 -e PDAC_SIGNUP_ENABLED=true ngoduykhanh/powerdns-nameserver
## [Azorian Solutions](https://azorian.solutions) Docker image strategy
The goal of creating this image and others alike is to provide a fairly uniform and turn-key implementation for a chosen set of products and solutions. By compiling the server binaries from source code, a greater chain of security is maintained by eliminating unnecessary trusts. This approach also helps assure support of specific features that may otherwise vary from distribution to distribution. A secondary goal of creating this image and others alike is to provide at least two Linux distribution options for any supported product or solution which is why you will often see tags for both Alpine Linux and Debian Linux.
All documentation will be written with the assumption that you are already reasonably familiar with this ecosystem. This includes container concepts, the Docker ecosystem, and more specifically the product or solution that you are deploying. Simply put, I won't be fluffing the docs with content explaining every detail of what is presented.
## Supported tags
\* denotes an image that is planned but has not yet been released.
### Alpine Linux
- main, latest
- 0.2.4-alpine, alpine, 0.2.4
### Debian Linux
- 0.2.4-debian, debian
## Deploying this image
### App configuration
Configuration of the PowerDNS Admin app may be achieved through a mixture of two approaches. With either approach you choose, you will need to be aware of the various settings available for the app.
[PowerDNS Admin Settings](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/docs/settings.md)
#### Approach #1
You may pass app settings as environment variables to the container. These environment variables will be automatically inserted into the app.config object. Any environment variable that begins with PDA_ or PDAC_ and is entirely capitalized, will be inserted into the app.config object.
**One key difference** between the two variable prefixes is that variables prefixed with PDAC_ will have the PDAC_ prefix removed before being inserted into the app.config object.
For example, say you pass the environment variable PDA_BIND_ADDRESS with the value 0.0.0.0 to the container.
PDA_BIND_ADDRESS=0.0.0.0
This will create a key named PDA_BIND_ADDRESS in app.config with the value of the environment variable value of 0.0.0.0.
Similar to how secrets work for Docker Swarm containers, you may append _FILE to your environment variables to automatically load the contents of the given file path into an environment variable.
**One key difference** here from Docker Swarm secrets is that you provide an absolute file path to any path of our choosing. Also, once the contents of the file are loaded, an environment variable of the same name without the trailing _FILE is created with the contents of the loaded file as the value. The original environment variable with the trailing _FILE is removed.
The following example illustrates a scenario where you want to set the PDA_PDNS_API_KEY app setting in a secure fashion. You have the secret API key stored at /run/secrets/PDA_PDNS_API_KEY.
PDA_PDNS_API_KEY_FILE=/run/secrets/PDA_PDNS_API_KEY
This would be the equivalent of running:
export PDA_PDNS_API_KEY=CONTENTS_OF_SECRET_FILE
#### Approach #2
With this approach, you may create traditional PowerDNS Admin config files and map them to an accessible location inside the container. Then you can pass an environment variable to indicate where to load the file from.
For example, say you have mapped a python configuration file to /srv/app/configs/my-config.py in the container. You would add the following environment variable to your deployment configuration:
FLASK_CONF=/srv/app/configs/my-config.py
**!!! NOTICE !!!** - There is a very specific order in which different sources are loaded. Take careful notice to avoid confusion!
- The config file **/srv/app/configs/default.py** is loaded.
- The **FLASK_CONF** environment variable is loaded if configured.
- Any instance specific configuration provided via code instantiation is loaded.
- Settings are loaded individually from any environment variables present.
- If all required MySQL settings are present in the app.config object and no SQLALCHEMY_DATABASE_URI app setting has been configured, the app.config setting will be automatically created from the provided MySQL settings.
- Otherwise, the SQLALCHEMY_DATABASE_URI app setting is set to "sqlite:////srv/app/pdns.db"
### Deploy with Docker Run
To run a simple container on Docker with this image, execute the following Docker command;
docker run -d -p 8080:80 -e PDAC_SIGNUP_ENABLED=true ngoduykhanh/powerdns-nameserver
### Deploy with Docker Compose
To run this image using Docker Compose, create a YAML file in a place of your choosing and add the following contents to it;
version: "3.3"
services:
powerdns-admin:
image: ngoduykhanh/powerdns-admin
restart: unless-stopped
environment:
- PDAC_SALT=ntR8BmMQb7MKTJ5YrgQwZr3pxU3uLG
- PDAC_SECRET_KEY=eJ92QLuennVStHTj5FVwxz7m4r3Ng3
ports:
- "8080:80"
Then execute the following Docker Compose command;
docker-compose -f /path/to/yaml/file.yml up
**!!! NOTICE !!!** - Make sure to set the environment variable `PDAC_SECRET_KEY` to a long random string ([see here for additional information](https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY))
## Building this image
If you want to build this image yourself, you can easily do so using the **docker/bin/build-image** command I have included.
The build-image command has the following parameter format;
build-image IMAGE_TAG_NAME APP_VERSION DISTRO_NAME DISTRO_TAG
So for example, to build the PowerDNS Admin app version 0.2.3 on Alpine Linux 3.14, you would execute the following shell command:
build-image 0.2.3-alpine-3.14 0.2.3 alpine 3.14
The build-image command assumes the following parameter defaults;
- Image Tag Name: latest
- App Version: 0.2.4
- Distro Name: alpine
- Distro Tag: 3.14
This means that running the build-image command with no parameters would be the equivalent of executing the following shell command:
build-image latest 0.2.4 alpine 3.14
When the image is tagged during compilation, the repository portion of the image tag is derived from the contents of the docker/repo.cfg file and the tag from the first parameter provided to the build-image command.

430
docs/settings.md Normal file
View file

@ -0,0 +1,430 @@
# PowerDNS-Admin Settings
The app has a plethora of settings that may be configured through a number of methods. Unfortunately, at the current state the app does not use a completely uniform naming scheme across configuration methods.
Don't worry though! A strong standard is quickly being implemented with full backwards compatibility so this problem should disappear quickly.
Let's break the application settings keys down into two primary categories, docker only and app settings. Below you will find a detailed description of each setting and how to use it.
## Docker Only Settings
\+ denotes a setting that is planned but not yet implemented.
#### PDA_UID = 1000
This setting controls what user ID is used for various container setup processes during image building. This setting is useful if you're mounting a host path over the /srv/app volume and need to match your host file system permissions.
#### PDA_USER = pda
This setting controls what username is used for various container setup processes during image building. This setting is useful if you're mounting a host path over the /srv/app volume and need to match your host file system permissions.
#### PDA_GID = 1000
This setting controls what group ID is used for various container setup processes during image building. This setting is useful if you're mounting a host path over the /srv/app volume and need to match your host file system permissions.
#### PDA_GROUP = pda
This setting controls what group name is used for various container setup processes during image building. This setting is useful if you're mounting a host path over the /srv/app volume and need to match your host file system permissions.
#### PDA_LOCAL_PATH
This setting controls with path on the Docker host machine will be used as the project root for local development.
## App Settings
\* denotes a legacy setting key that will be deprecated.
\+ denotes a setting that is planned but not yet implemented.
**!COMPUTED!** denotes a default setting value that is computed dynamically at the time it is set.
#### PDA_BIND_ADDRESS = 0.0.0.0
The container interface IP mask to bind both gunicorn and the built-in web server to.
#### PDA_BIND_PORT = 80
The container port to bind both gunicorn and the built-in web server to.
#### PDA_DEBUG = false
The debug mode setting of the app.
#### PDA_LOAD_ENV_FILES = true
Whether to automatically convert environment variables beginning with PDA_ and ending with _FILE.
#### PDA_AUTOINIT_ENABLED = true
Whether the automatic first-time initialization feature is enabled.
#### PDA_AUTOINIT_FORCE = false
Whether to force execution of the first-time initialization feature.
#### PDA_CHECK_MYSQL_ENABLED = false
Whether to delay container startup until the configured MySQL server is online.
#### PDA_CHECK_MYSQL_FAIL_DELAY = 2
How many seconds to wait after a MySQL connection attempt failure before trying again.
#### PDA_CHECK_MYSQL_SUCCESS_DELAY = 0
How many seconds to wait after a successful MySQL connection attempt before proceeding to the next step.
#### PDA_CHECK_MYSQL_ATTEMPTS = 30
How many MySQL connection attempts should be made before halting container execution.
#### PDA_CHECK_API_ENABLED = false
Whether to delay container startup until the configured PDNS API key passes authorization to the server.
#### PDA_CHECK_API_FAIL_DELAY = 2
How many seconds to wait after an API connection attempt failure before trying again.
#### PDA_CHECK_API_SUCCESS_DELAY = 0
How many seconds to wait after an API successful connection attempt before proceeding to the next step.
#### PDA_CHECK_API_ATTEMPTS = 15
How many API connection attempts should be made before halting container execution.
#### PDA_CHECK_PYTEST_ENABLED = false
Whether to run the app's Python test unit before starting the primary container process.
#### PDA_GUNICORN_ENABLED = false
Whether to enable the use of gunicorn.
#### PDA_GUNICORN_TIMEOUT = 120
The request timeout in seconds for a gunicorn worker.
#### PDA_GUNICORN_WORKERS = 4
The total number of gunicorn worker threads to spawn.
#### PDA_GUNICORN_LOGLEVEL = info
The log output level of the gunicorn process.
#### PDA_GUNICORN_ARGS = !COMPUTED!
The additional gunicorn arguments that will be appended to the startup command when gunicorn is enabled.
#### PDA_PDNS_API_KEY = CHANGE_ME!
The API key for the associated PowerDNS Authoritative server API.
#### PDA_PDNS_API_VERSION = 4.5.2
The API version of the associated PowerDNS Authoritative server API.
#### PDA_PDNS_API_PROTO = http
The HTTP protocol for the associated PowerDNS Authoritative server API.
#### PDA_PDNS_API_HOST = 127.0.0.1
The hostname or IP address of the associated PowerDNS Authoritative server API.
#### PDA_PDNS_API_PORT = 8081
The port of the associated PowerDNS Authoritative server API.
#### + PDA_ADMIN_USER_USERNAME
The username to use for automatic admin account setup on startup.
#### + PDA_ADMIN_USER_PASSWORD
The password to use for automatic admin account setup on startup.
#### * FLASK_ENV = production
The Flask app environment classification.
**!!! NOTICE !!!** - Setting this to development carries implicit behaviors such as automatically setting the FLASK_DEBUG setting to true.
#### * FLASK_DEBUG = false
The Flask app debug mode setting.
**!!! NOTICE !!!** - Enabling debug mode will cause the debugger to run during app initialization.
#### * FLASK_APP = /srv/app/run.py
The Flask Python file that should be treated as the main app file.
#### * FLASK_CONF
The Flask Python config file path that should be loaded during app initialization.
#### * PDNS_ADMIN_LOG_LEVEL = WARNING
The log output level that the app logging mechanism will use.
#### * LOG_LEVEL = ?
TODO: Find out where this is used and what it's default value is
#### * BIND_ADDRESS = 127.0.0.1
Refer to the PDA_BIND_ADDRESS setting documentation.
#### * PORT = 9191
Refer to the PDA_BIND_PORT setting documentation.
#### * DEBUG_MODE = false
Refer to the PDA_DEBUG setting documentation.
#### * SALT = ?
TODO: Find out where this is used and what it's default value is
TODO: Reassign to PDA_ scoped key
#### * SECRET_KEY = ?
This should be set to a long, random string. [[see here for additional information]](https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY)
TODO: Reassign to PDA_ scoped key
#### * SIGNUP_ENABLED = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * HSTS_ENABLED = false
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * OFFLINE_MODE = false
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * FILE_SYSTEM_SESSIONS_ENABLED = false
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * LOCAL_DB_ENABLED = false
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * LDAP_ENABLED = false
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SQLA_DB_USER = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SQLA_DB_PASSWORD = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SQLA_DB_HOST = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SQLA_DB_PORT = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SQLA_DB_NAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SQLALCHEMY_DATABASE_URI = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SQLALCHEMY_TRACK_MODIFICATIONS = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_SERVER = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_PORT = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_DEBUG = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_USE_TLS = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_USE_SSL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_USERNAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_PASSWORD = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * MAIL_DEFAULT_SENDER = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * REMOTE_USER_LOGOUT_URL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * REMOTE_USER_COOKIES = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * OIDC_OAUTH_API_URL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * OIDC_OAUTH_TOKEN_URL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * OIDC_OAUTH_AUTHORIZE_URL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ENABLED = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_DEBUG = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_PATH = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ASSERTION_ENCRYPTED = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_METADATA_URL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_METADATA_CACHE_LIFETIME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_IDP_SSO_BINDING = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_IDP_ENTITY_ID = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_NAMEID_FORMAT = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_EMAIL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_GIVENNAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_SURNAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_NAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_USERNAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_ADMIN = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_GROUP = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_CERT = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_KEY = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_GROUP_ADMIN_NAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_GROUP_TO_ACCOUNT_MAPPING = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_ATTRIBUTE_ACCOUNT = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_SP_ENTITY_ID = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_SP_CONTACT_NAME = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_SP_CONTACT_MAIL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_SIGN_REQUEST = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_WANT_MESSAGE_SIGNED = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_LOGOUT = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key
#### * SAML_LOGOUT_URL = ?
TODO: Complete the setting description
TODO: Reassign to PDA_ scoped key

View file

@ -5,6 +5,7 @@
"bootstrap-datepicker": "^1.8.0",
"bootstrap-validator": "^0.11.9",
"datatables.net-plugins": "^1.10.19",
"font-awesome": "^4.7.0",
"icheck": "^1.0.2",
"jquery-slimscroll": "^1.3.8",
"jquery-ui-dist": "^1.12.1",

View file

@ -9,11 +9,17 @@ from flask_session import Session
from .lib import utils
def create_app(config=None):
def create_app(config=None, other=''):
import urllib
from . import models, routes, services
from .assets import assets
from .lib import config_util
app = Flask(__name__)
########################################
# LOGGING SETUP
########################################
# Read log level from environment variable
log_level_name = os.environ.get('PDNS_ADMIN_LOG_LEVEL', 'WARNING')
log_level = logging.getLevelName(log_level_name.upper())
@ -23,17 +29,18 @@ def create_app(config=None):
format=
"[%(asctime)s] [%(filename)s:%(lineno)d] %(levelname)s - %(message)s")
# If we use Docker + Gunicorn, adjust the
# log handler
if "GUNICORN_LOGLEVEL" in os.environ:
# Reconfigure the log handler if gunicorn is being ran in Docker
if "PDA_GUNICORN_ENABLED" in os.environ and bool(os.environ['PDA_GUNICORN_ENABLED']):
gunicorn_logger = logging.getLogger("gunicorn.error")
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)
# Proxy
# Proxy Support
app.wsgi_app = ProxyFix(app.wsgi_app)
# CSRF protection
########################################
# CSRF Protection Setup
########################################
csrf = SeaSurf(app)
csrf.exempt(routes.index.dyndns_checkip)
csrf.exempt(routes.index.dyndns_update)
@ -58,38 +65,71 @@ def create_app(config=None):
csrf.exempt(routes.api.api_zone_cryptokeys)
csrf.exempt(routes.api.api_zone_cryptokey)
# Load config from env variables if using docker
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
app.config.from_object('powerdnsadmin.docker_config')
else:
# Load default configuration
app.config.from_object('powerdnsadmin.default_config')
########################################
# CONFIGURATION SETUP
########################################
# Load config file from FLASK_CONF env variable
# Load default configuration
app.config.from_object('configs.default')
# Load config file from path given in FLASK_CONF env variable
if 'FLASK_CONF' in os.environ:
app.config.from_envvar('FLASK_CONF')
# Load app sepecified configuration
# Load instance specific configuration
if config is not None:
if isinstance(config, dict):
app.config.update(config)
elif config.endswith('.py'):
app.config.from_pyfile(config)
# Load configuration from environment variables
config_util.load_config_from_env(app)
# If no SQLA DB URI is set but SQLA MySQL settings are present, then create DB URI setting
if 'SQLALCHEMY_DATABASE_URI' not in app.config and 'SQLA_DB_USER' in app.config \
and 'SQLA_DB_PASSWORD' in app.config and 'SQLA_DB_HOST' in app.config and 'SQLA_DB_PORT' in app.config \
and 'SQLA_DB_NAME' in app.config:
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://{}:{}@{}:{}/{}'.format(
urllib.parse.quote_plus(app.config.get('SQLA_DB_USER')),
urllib.parse.quote_plus(app.config.get('SQLA_DB_PASSWORD')),
app.config.get('SQLA_DB_HOST'),
app.config.get('SQLA_DB_PORT'),
app.config.get('SQLA_DB_NAME')
)
else:
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////srv/app/pdns.db'
########################################
# SSL SETUP
########################################
# HSTS
if app.config.get('HSTS_ENABLED'):
from flask_sslify import SSLify
_sslify = SSLify(app) # lgtm [py/unused-local-variable]
########################################
# SESSION SETUP
########################################
# Load Flask-Session
if app.config.get('FILESYSTEM_SESSIONS_ENABLED'):
app.config['SESSION_TYPE'] = 'filesystem'
sess = Session()
sess.init_app(app)
########################################
# MAIL SETUP
########################################
# SMTP
app.mail = Mail(app)
########################################
# FLASK APP SETUP
########################################
# Load app's components
assets.init_app(app)
models.init_app(app)

View file

@ -1,34 +0,0 @@
import os
import urllib.parse
basedir = os.path.abspath(os.path.dirname(__file__))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
HSTS_ENABLED = False
OFFLINE_MODE = False
FILESYSTEM_SESSIONS_ENABLED = False
### DATABASE CONFIG
SQLA_DB_USER = 'pda'
SQLA_DB_PASSWORD = 'changeme'
SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_NAME = 'pda'
SQLALCHEMY_TRACK_MODIFICATIONS = True
### DATABASE - MySQL
SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
urllib.parse.quote_plus(SQLA_DB_USER),
urllib.parse.quote_plus(SQLA_DB_PASSWORD),
SQLA_DB_HOST,
SQLA_DB_NAME
)
### DATABASE - SQLite
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# SAML Authnetication
SAML_ENABLED = False
SAML_ASSERTION_ENCRYPTED = True

26
powerdnsadmin/env/boolean.env vendored Normal file
View file

@ -0,0 +1,26 @@
PDA_DEBUG
PDA_GUNICORN_ENABLED
PDA_LOAD_ENV_FILES
PDA_AUTOINIT_ENABLED
PDA_AUTOINIT_FORCE
PDA_CHECK_MYSQL_ENABLED
PDA_CHECK_API_ENABLED
PDA_CHECK_PYTEST_ENABLED
DEBUG_MODE
SIGNUP_ENABLED
HSTS_ENABLED
OFFLINE_MODE
FILESYSTEM_SESSIONS_ENABLED
SQLALCHEMY_TRACK_MODIFICATIONS
LOCAL_DB_ENABLED
REMOTE_USER_ENABLED
LDAP_ENABLED
MAIL_DEBUG
MAIL_USE_TLS
MAIL_USE_SSL
SAML_ENABLED
SAML_DEBUG
SAML_SIGN_REQUEST
SAML_WANT_MESSAGE_SIGNED
SAML_LOGOUT
SAML_ASSERTION_ENCRYPTED

31
powerdnsadmin/env/default.env vendored Normal file
View file

@ -0,0 +1,31 @@
PDA_UID
PDA_USER
PDA_GID
PDA_GROUP
PDA_BIND_ADDRESS
PDA_BIND_PORT
PDA_DEBUG
PDA_LOAD_ENV_FILES
PDA_AUTOINIT_ENABLED
PDA_AUTOINIT_FORCE
PDA_CHECK_MYSQL_ENABLED
PDA_CHECK_MYSQL_FAIL_DELAY
PDA_CHECK_MYSQL_SUCCESS_DELAY
PDA_CHECK_MYSQL_ATTEMPTS
PDA_CHECK_API_ENABLED
PDA_CHECK_API_FAIL_DELAY
PDA_CHECK_API_SUCCESS_DELAY
PDA_CHECK_API_ATTEMPTS
PDA_CHECK_PYTEST_ENABLED
PDA_GUNICORN_ENABLED
PDA_GUNICORN_TIMEOUT
PDA_GUNICORN_WORKERS
PDA_GUNICORN_LOGLEVEL
PDA_GUNICORN_ARGS
PDA_PDNS_API_KEY
PDA_PDNS_API_VERSION
PDA_PDNS_API_PROTO
PDA_PDNS_API_HOST
PDA_PDNS_API_PORT
PDA_ADMIN_USER_USERNAME
PDA_ADMIN_USER_PASSWORD

75
powerdnsadmin/env/discovered.env vendored Normal file
View file

@ -0,0 +1,75 @@
LC_ALL
LANG
LANGUAGE
SERVER_NAME
BIND_ADDRESS
PORT
DEBUG_MODE
SALT
SECRET_KEY
SESSION_TYPE
PDNS_ADMIN_LOG_LEVEL
OFFLINE_MODE
FILESYSTEM_SESSIONS_ENABLED
HSTS_ENABLED
GUNICORN_LOGLEVEL
GUNICORN_WORKERS
GUNICORN_TIMEOUT
GUNICORN_ARGS
FLASK_APP
FLASK_ENV
FLASK_CONF
SQLA_DB_HOST
SQLA_DB_PORT
SQLA_DB_USER
SQLA_DB_PASSWORD
SQLA_DB_NAME
SQLALCHEMY_TRACK_MODIFICATIONS
SQLALCHEMY_DATABASE_URI
MAIL_SERVER
MAIL_PORT
MAIL_DEBUG
MAIL_USE_TLS
MAIL_USE_SSL
MAIL_USERNAME
MAIL_PASSWORD
MAIL_DEFAULT_SENDER
SAML_ENABLED
SAML_DEBUG
SAML_PATH
SAML_METADATA_URL
SAML_METADATA_CACHE_LIFETIME
SAML_IDP_SSO_BINDING
SAML_IDP_ENTITY_ID
SAML_NAMEID_FORMAT
SAML_SP_REQUESTED_ATTRIBUTES
SAML_ASSERTION_ENCRYPTED
SAML_ATTRIBUTE_EMAIL
SAML_ATTRIBUTE_GIVENNAME
SAML_ATTRIBUTE_SURNAME
SAML_ATTRIBUTE_USERNAME
SAML_ATTRIBUTE_ADMIN
SAML_ATTRIBUTE_ACCOUNT
SAML_SP_ENTITY_ID
SAML_SP_CONTACT_NAME
SAML_SP_CONTACT_MAIL
SAML_CERT_FILE
SAML_CERT_KEY
SAML_SIGN_REQUEST
SAML_LOGOUT
SAML_LOGOUT_URL
REMOTE_USER_ENABLED
REMOTE_USER_LOGOUT_URL
REMOTE_USER_COOKIES

8
powerdnsadmin/env/integer.env vendored Normal file
View file

@ -0,0 +1,8 @@
PDA_UID
PDA_GID
PDA_BIND_PORT
PDA_PDNS_PORT
PORT
SQLA_DB_PORT
MAIL_PORT
SAML_METADATA_CACHE_LIFETIME

61
powerdnsadmin/env/legacy.env vendored Normal file
View file

@ -0,0 +1,61 @@
PDNS_ADMIN_LOG_LEVEL
LOG_LEVEL
BIND_ADDRESS
PORT
DEBUG_MODE
SALT
SECRET_KEY
SIGNUP_ENABLED
HSTS_ENABLED
OFFLINE_MODE
FILESYSTEM_SESSIONS_ENABLED
LOCAL_DB_ENABLED
LDAP_ENABLED
SQLA_DB_USER
SQLA_DB_PASSWORD
SQLA_DB_HOST
SQLA_DB_PORT
SQLA_DB_NAME
SQLALCHEMY_DATABASE_URI
SQLALCHEMY_TRACK_MODIFICATIONS
MAIL_SERVER
MAIL_PORT
MAIL_DEBUG
MAIL_USE_TLS
MAIL_USE_SSL
MAIL_USERNAME
MAIL_PASSWORD
MAIL_DEFAULT_SENDER
REMOTE_USER_LOGOUT_URL
REMOTE_USER_COOKIES
OIDC_OAUTH_API_URL
OIDC_OAUTH_TOKEN_URL
OIDC_OAUTH_AUTHORIZE_URL
SAML_ENABLED
SAML_DEBUG
SAML_PATH
SAML_ASSERTION_ENCRYPTED
SAML_METADATA_URL
SAML_METADATA_CACHE_LIFETIME
SAML_IDP_SSO_BINDING
SAML_IDP_ENTITY_ID
SAML_NAMEID_FORMAT
SAML_ATTRIBUTE_EMAIL
SAML_ATTRIBUTE_GIVENNAME
SAML_ATTRIBUTE_SURNAME
SAML_ATTRIBUTE_NAME
SAML_ATTRIBUTE_USERNAME
SAML_ATTRIBUTE_ADMIN
SAML_ATTRIBUTE_GROUP
SAML_CERT
SAML_KEY
SAML_GROUP_ADMIN_NAME
SAML_GROUP_TO_ACCOUNT_MAPPING
SAML_ATTRIBUTE_ACCOUNT
SAML_SP_ENTITY_ID
SAML_SP_CONTACT_NAME
SAML_SP_CONTACT_MAIL
SAML_SIGN_REQUEST
SAML_WANT_MESSAGE_SIGNED
SAML_LOGOUT
SAML_LOGOUT_URL

View file

@ -0,0 +1,55 @@
import os
def str2bool(v):
return v.lower() in ("true", "yes", "1")
def load_env_file(file_name='legacy'):
f = open('/srv/app/powerdnsadmin/env/' + file_name + '.env', 'r')
lines = f.read().splitlines()
f.close()
return lines
def load_config_from_env(app):
legacy_vars = load_env_file('legacy')
boolean_vars = load_env_file('boolean')
integer_vars = load_env_file('integer')
config = {}
for key in os.environ:
config_key = None
val = None
if (key.startswith('PDA_') or key in legacy_vars) and not key.endswith('_FILE'):
config_key = key
val = os.environ[key]
if key.startswith('PDAC_') and not key.endswith('_FILE'):
config_key = key[5:]
val = os.environ[key]
if key.endswith('_FILE'):
config_key = key[:-5]
if key.startswith('PDAC_'):
config_key = config_key[5:]
if key.startswith('PDAC_') or config_key in legacy_vars:
with open(os.environ[key]) as f:
val = f.read()
f.close()
if val is not None:
if config_key in boolean_vars:
val = str2bool(val)
if config_key in integer_vars:
val = int(val)
if config_key is not None:
config[config_key] = val
if isinstance(config, dict):
app.config.update(config)

View file

@ -28,3 +28,4 @@ PyYAML==5.4
Flask-SSLify==0.1.5
Flask-Mail==0.9.1
flask-session==0.3.2
xmlsec==1.3.12

6
run.py
View file

@ -3,4 +3,8 @@ from powerdnsadmin import create_app
if __name__ == '__main__':
app = create_app()
app.run(debug = True, host=app.config.get('BIND_ADDRESS', '127.0.0.1'), port=app.config.get('PORT', '9191'))
app.run(
host=app.config.get('BIND_ADDRESS', app.config.get('PDA_BIND_ADDRESS', '0.0.0.0')),
port=app.config.get('PORT', app.config.get('PDA_BIND_PORT', 80)),
debug=app.config.get('DEBUG_MODE', app.config.get('PDA_DEBUG', False))
)

1151
yarn.lock

File diff suppressed because it is too large Load diff