Custom certificate support. Wildcard domain support
This commit is contained in:
parent
52aa04c59c
commit
a406e895d2
12 changed files with 393 additions and 202 deletions
15
.github/workflows/bouncer.yml
vendored
15
.github/workflows/bouncer.yml
vendored
|
|
@ -22,6 +22,12 @@ jobs:
|
|||
name: Bake Bouncer Container
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Setup: Checkout Source"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
bouncer
|
||||
|
||||
- name: "Setup: Get Date"
|
||||
id: date
|
||||
run: |
|
||||
|
|
@ -35,7 +41,7 @@ jobs:
|
|||
- name: "Setup: PHP"
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.1
|
||||
php-version: 8.2
|
||||
env:
|
||||
runner: self-hosted
|
||||
|
||||
|
|
@ -61,12 +67,6 @@ jobs:
|
|||
username: matthewbaggett
|
||||
password: ${{ secrets.GHCR_PASSWORD }}
|
||||
|
||||
- name: "Setup: Checkout Source"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
bouncer
|
||||
|
||||
- name: "Setup: Configure Cache"
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
|
@ -90,6 +90,7 @@ jobs:
|
|||
push: true
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
GIT_BUILD_ID=${{ github.ref_name }}
|
||||
BUILD_DATE=${{ steps.date.outputs.container_build_datetime }}
|
||||
GIT_COMMIT_MESSAGE=${{ github.event.head_commit.message }}
|
||||
tags: |
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
|
||||
FROM php:cli as bouncer
|
||||
LABEL maintainer="Matthew Baggett <matthew@baggett.me>" \
|
||||
org.label-schema.vcs-url="https://github.com/benzine-framework/docker" \
|
||||
org.opencontainers.image.source="https://github.com/benzine-framework/docker"
|
||||
|
||||
USER root
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# ts:skip=AC_DOCKER_0002 Mis-detecting usage of apt instead of apt-get
|
||||
# Install nginx, certbot
|
||||
RUN apt-get -qq update && \
|
||||
|
|
@ -39,12 +41,12 @@ COPY self-signed-certificates /certs
|
|||
|
||||
# Install runits for services
|
||||
COPY nginx.runit /etc/service/nginx/run
|
||||
COPY logs.runit /etc/service/nginx-logs/run
|
||||
COPY logs.finish /etc/service/nginx-logs/finish
|
||||
#COPY logs.runit /etc/service/nginx-logs/run
|
||||
#COPY logs.finish /etc/service/nginx-logs/finish
|
||||
COPY bouncer.runit /etc/service/bouncer/run
|
||||
COPY bouncer.finish /etc/service/bouncer/finish
|
||||
COPY logs-nginx-access.runit /etc/service/logs-nginx-access/run
|
||||
COPY logs-nginx-error.runit /etc/service/logs-nginx-error/run
|
||||
#COPY logs-nginx-access.runit /etc/service/logs-nginx-access/run
|
||||
#COPY logs-nginx-error.runit /etc/service/logs-nginx-error/run
|
||||
RUN chmod +x /etc/service/*/run /etc/service/*/finish
|
||||
|
||||
# Copy default nginx bits
|
||||
|
|
@ -69,6 +71,16 @@ COPY src /app/src
|
|||
COPY templates /app/templates
|
||||
RUN chmod +x /app/bin/bouncer
|
||||
|
||||
# stuff some envs from build
|
||||
ARG BUILD_DATE
|
||||
ARG GIT_SHA
|
||||
ARG GIT_BUILD_ID
|
||||
ARG GIT_COMMIT_MESSAGE
|
||||
ENV BUILD_DATE=${BUILD_DATE} \
|
||||
GIT_SHA=${GIT_SHA} \
|
||||
GIT_BUILD_ID=${GIT_BUILD_ID} \
|
||||
GIT_COMMIT_MESSAGE=${GIT_COMMIT_MESSAGE}
|
||||
|
||||
# Create some volumes for logs and certs
|
||||
VOLUME /etc/letsencrypt
|
||||
VOLUME /var/log/bouncer
|
||||
|
|
@ -77,32 +89,26 @@ VOLUME /var/log/bouncer
|
|||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
# Down-privelege to bouncer
|
||||
USER bouncer
|
||||
|
||||
# Set a healthcheck to curl the bouncer and expect a 200
|
||||
HEALTHCHECK --start-period=30s \
|
||||
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
|
||||
|
||||
# stuff some envs from build
|
||||
ARG BUILD_DATE
|
||||
ARG GIT_SHA
|
||||
ARG GIT_COMMIT_MESSAGE
|
||||
ENV BUILD_DATE=${BUILD_DATE} \
|
||||
GIT_SHA=${GIT_SHA} \
|
||||
GIT_COMMIT_MESSAGE=${GIT_COMMIT_MESSAGE}
|
||||
RUN printenv | sort
|
||||
|
||||
FROM benzine/php:nginx-8.1 as test-app-a
|
||||
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
|
||||
FROM php:nginx as test-app-a
|
||||
COPY ./test/public-web-a /app/public
|
||||
HEALTHCHECK --start-period=30s \
|
||||
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
|
||||
|
||||
FROM benzine/php:nginx-8.1 as test-app-b
|
||||
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
|
||||
FROM php:nginx as test-app-b
|
||||
COPY ./test/public-web-b /app/public
|
||||
HEALTHCHECK --start-period=30s \
|
||||
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
|
||||
|
||||
FROM benzine/php:nginx-8.1 as test-app-c
|
||||
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
|
||||
FROM php:nginx as test-app-c
|
||||
COPY ./test/public-web-c /app/public
|
||||
HEALTHCHECK --start-period=30s \
|
||||
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
|
||||
|
|
|
|||
|
|
@ -4,17 +4,21 @@ php-cs-fixer:
|
|||
|
||||
fix: php-cs-fixer
|
||||
|
||||
image_reference_devel:=ghcr.io/benzine-framework/bouncer:devel
|
||||
|
||||
build-n-push: fix
|
||||
docker build \
|
||||
--build-arg BUILD_DATE="$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||
--build-arg GIT_SHA="$(shell git rev-parse HEAD)" \
|
||||
--build-arg GIT_BUILD_ID="$(shell git rev-parse --abbrev-ref HEAD)-$(shell git describe --tags --dirty --always | sed -e 's/^v//')" \
|
||||
--build-arg GIT_COMMIT_MESSAGE="$(shell git log -1 --pretty=%B | head -n1)" \
|
||||
--build-context php:cli=docker-image://ghcr.io/benzine-framework/php:cli-8.2 \
|
||||
--tag benzine/bouncer \
|
||||
--tag ghcr.io/benzine-framework/bouncer \
|
||||
--tag $(image_reference_devel) \
|
||||
--target bouncer \
|
||||
--progress plain \
|
||||
.
|
||||
#docker push benzine/bouncer
|
||||
docker push ghcr.io/benzine-framework/bouncer
|
||||
docker push $(image_reference_devel)
|
||||
|
||||
test-as-service: clean
|
||||
docker build -t bouncer --target bouncer .
|
||||
|
|
|
|||
|
|
@ -9,19 +9,20 @@ These should not be confused.
|
|||
|
||||
#### Main configuration
|
||||
|
||||
| Key | Default | Options | Behaviour |
|
||||
| --------------------------------------------- | ----------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --- |
|
||||
| DOCKER_HOST | false | | Define a http endpoint representing your docker socket. If this is null, it connects to /var/lib/docker.sock |
|
||||
| GLOBAL_CERT | false | Contents of an ssl certificate | If you want to provide a single cert for all endpoints, perhaps with a catch-all that may be later overriden, you can provide the whole contents of a certificates file here. |
|
||||
| GLOBAL_CERT_KEY | false | Contents of an ssl certificates private key | The private key related to GLOBAL CERT. These must be provided in tandem. |
|
||||
| BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS | false | positive numbers | To force the bouncer to update on a schedule even if no changes are detected, measured in seconds |
|
||||
| BOUNCER_MAXIMUM_NGINX_CONFIG_CREATION_NOTICES | 15 | positive numbers | To limit the number lines of output regarding which domains have been configured. Any more domains than this count, and none will be output, instead replaced by "More than 15 Nginx configs generated.. Too many to show them all!" |
|
||||
| LOG_NAME | bouncer | | The name of the log file to write to |
|
||||
| LOG_FILE | /var/log/bouncer/bouncer.log | | The path to the log file to write to |
|
||||
| LOG_LEVEL | debug | info, debug, critical etc | The level of logging to write to the log file. See Monolog docs. |
|
||||
| LOG_LEVEL_NAME_LENGTH | 4 | positive numbers | The length of the level name to be written to the log file. See Monolog docs. |
|
||||
| LOG_LINE_FORMAT | [%datetime%] %level_name%: %channel%: %message% | | The format of the log line. See Monolog docs. |
|
||||
| LOG_COLOUR | true | true, false | Whether to colourise the log output sent to stdout. | |
|
||||
| Key | Default | Options | Behaviour |
|
||||
| --------------------------------------------- | ----------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| DOCKER_HOST | false | | Define a http endpoint representing your docker socket. If this is null, it connects to /var/lib/docker.sock |
|
||||
| GLOBAL_CERT | false | Contents of an ssl certificate | If you want to provide a single cert for all endpoints, perhaps with a catch-all that may be later overriden, you can provide the whole contents of a certificates file here. |
|
||||
| GLOBAL_CERT_KEY | false | Contents of an ssl certificates private key | The private key related to GLOBAL CERT. These must be provided in tandem. |
|
||||
| BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS | 0 | positive numbers | To force the bouncer to update on a schedule even if no changes are detected, measured in seconds |
|
||||
| BOUNCER_MAXIMUM_NGINX_CONFIG_CREATION_NOTICES | 15 | positive numbers | To limit the number lines of output regarding which domains have been configured. Any more domains than this count, and none will be output, instead replaced by "More than 15 Nginx configs generated.. Too many to show them all!" |
|
||||
| BOUNCER_ALLOW_NON_SSL | Defaults to enabled. | Values are "yes" or "true", anything else is false | By default, should HTTP only traffic be allowed to hit this service? If disabled, http traffic is forwarded towards https. This can be over-ridden per-service with the same env. |
|
||||
| LOG_NAME | bouncer | | The name of the log file to write to |
|
||||
| LOG_FILE | /var/log/bouncer/bouncer.log | | The path to the log file to write to |
|
||||
| LOG_LEVEL | debug | info, debug, critical etc | The level of logging to write to the log file. See Monolog docs. |
|
||||
| LOG_LEVEL_NAME_LENGTH | 4 | positive numbers | The length of the level name to be written to the log file. See Monolog docs. |
|
||||
| LOG_LINE_FORMAT | [%datetime%] %level_name%: %channel%: %message% | | The format of the log line. See Monolog docs. |
|
||||
| LOG_COLOUR | true | true, false | Whether to colourise the log output sent to stdout. |
|
||||
|
||||
#### For using with Lets Encrypt:tm:
|
||||
|
||||
|
|
@ -33,10 +34,10 @@ These should not be confused.
|
|||
#### For using S3-compatable storage for generated cert synchronisation with Lets Encrypt
|
||||
|
||||
| Key | Default | Options | Behaviour |
|
||||
| ---------------------------------- | ------- | --------------- | ------------------------------------------------------------------------------------- | --- |
|
||||
| ---------------------------------- | ------- | --------------- | ------------------------------------------------------------------------------------- |
|
||||
| BOUNCER_S3_BUCKET | false | | enable S3 behaviour to store lets-encrypt generated certs |
|
||||
| BOUNCER_S3_ENDPOINT | false | | define s3 endpoint to override default AWS s3 implementation, for example, with minio |
|
||||
| BOUNCER_S3_KEY_ID | false | | S3 API Key ID | |
|
||||
| BOUNCER_S3_KEY_ID | false | | S3 API Key ID |
|
||||
| BOUNCER_s3_KEY_SECRET | false | | S3 API Key Secret |
|
||||
| BOUNCER_S3_REGION | false | | S3 API Region |
|
||||
| BOUNCER_S3_USE_PATH_STYLE_ENDPOINT | false | `true or false` | Needed for minio |
|
||||
|
|
@ -50,9 +51,12 @@ These environment variables need to be applied to the CONSUMING SERVICE and not
|
|||
| ------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| BOUNCER_DOMAIN | "a.example.com" | The domain that should be directed to this container |
|
||||
| BOUNCER_LABEL | "MyService" | The label that should be directed to this container |
|
||||
| BOUNCER_IGNORE | not set | If set, the bouncer will ignore this service. Useful for services that are not ready to be exposed yet, or inherit BOUNCER_DOMAIN but need to be quashed. |
|
||||
| BOUNCER_AUTH | "username:password" e.g "root:toor" | Add a HTTP BASIC auth requirement to this hostname. |
|
||||
| BOUNCER_HOST_OVERRIDE | "localhost:80" | Override the host header that is sent to the service. Useful for services that are not aware of their own hostname, or annoying things like [mitmproxy](https://github.com/mitmproxy/mitmproxy/issues/3234) |
|
||||
| BOUNCER_LETSENCRYPT | Values are "yes" or "true", anything else is false | To enable, or disable Lets Encrypt service for this hostname |
|
||||
| BOUNCER_CERT | "-----BEGIN CERTIFICATE-----\nMIIFfTCCBGWgAwIBAgIQCgFBQgAAABAhQ..." | The contents of a custom certificate to use for this hostname. |
|
||||
| BOUNCER_CERT_KEY | "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCB..." | The contents of a custom private key to use for this hostname. |
|
||||
| BOUNCER_TARGET_PORT | 9000 | Explicitly define the port you want to hit the service on, in case of ambiguity |
|
||||
| BOUNCER_ALLOW_NON_SSL | Defaults to enabled. Values are "yes" or "true", anything else is false | Should HTTP only traffic be allowed to hit this service? If disabled, http traffic is forwarded towards https |
|
||||
| BOUNCER_ALLOW_WEBSOCKETS | Defaults to enabled. Values are "yes" or "true", anything else is false | Enable websocket behaviour |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
sleep 3
|
||||
printf "\n\n\n\n"
|
||||
echo "Starting Bouncer"
|
||||
/app/bin/bouncer
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
version: "3.4"
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
bouncer:
|
||||
image: bouncer
|
||||
image: ghcr.io/benzine-framework/bouncer:devel
|
||||
build:
|
||||
context: .
|
||||
target: bouncer
|
||||
additional_contexts:
|
||||
- php:cli=docker-image://ghcr.io/benzine-framework/php:cli-8.2
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./:/app
|
||||
|
|
@ -26,6 +28,8 @@ services:
|
|||
build:
|
||||
context: .
|
||||
target: test-app-a
|
||||
additional_contexts:
|
||||
- php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2
|
||||
volumes:
|
||||
- ./test/public-web-a:/app/public
|
||||
environment:
|
||||
|
|
@ -38,6 +42,8 @@ services:
|
|||
build:
|
||||
context: .
|
||||
target: test-app-b
|
||||
additional_contexts:
|
||||
- php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2
|
||||
volumes:
|
||||
- ./test/public-web-b:/app/public
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
#if [[ -f /var/log/bouncer/bouncer.log ]]; then
|
||||
# tail -f /var/log/bouncer/bouncer.log
|
||||
#fi
|
||||
if [[ -f /var/log/bouncer/bouncer.log ]]; then
|
||||
tail -f /var/log/bouncer/bouncer.log
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ namespace Bouncer;
|
|||
use AdamBrett\ShellWrapper\Command\Builder as CommandBuilder;
|
||||
use AdamBrett\ShellWrapper\Runners\Exec;
|
||||
use Aws\S3\S3Client;
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client as Guzzle;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
|
|
@ -21,6 +20,9 @@ use Bouncer\Logger\Formatter;
|
|||
use Spatie\Emoji\Emoji;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Twig\Environment as Twig;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Loader\FilesystemLoader as TwigLoader;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Monolog\Processor;
|
||||
|
|
@ -107,32 +109,6 @@ class Bouncer
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Allow defined global cert if set
|
||||
if (isset($this->environment['GLOBAL_CERT'], $this->environment['GLOBAL_CERT_KEY'])) {
|
||||
$this->setUseGlobalCert(true);
|
||||
$this->providedCertificateStore->write('global.crt', str_replace('\\n', "\n", trim($this->environment['GLOBAL_CERT'], '"')));
|
||||
$this->providedCertificateStore->write('global.key', str_replace('\\n', "\n", trim($this->environment['GLOBAL_CERT_KEY'], '"')));
|
||||
$this->logger->info("GLOBAL_CERT was set, so we're going to use a defined certificate!", ['emoji' => Emoji::globeShowingEuropeAfrica()]);
|
||||
}
|
||||
|
||||
// Determine forced update interval.
|
||||
if (isset($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS']) && is_numeric($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS'])) {
|
||||
$this->setForcedUpdateIntervalSeconds($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS']);
|
||||
}
|
||||
if ($this->getForcedUpdateIntervalSeconds() > 0) {
|
||||
$this->logger->warning(' Forced update interval is every {interval_seconds} seconds', ['emoji' => Emoji::watch(), 'interval_seconds' => $this->getForcedUpdateIntervalSeconds()]);
|
||||
} else {
|
||||
$this->logger->info(' Forced update interval is disabled', ['emoji' => Emoji::watch()]);
|
||||
}
|
||||
|
||||
// Determine maximum notices for nginx config creation.
|
||||
if (isset($this->environment['BOUNCER_MAXIMUM_NGINX_CONFIG_CREATION_NOTICES']) && is_numeric($this->environment['BOUNCER_MAXIMUM_NGINX_CONFIG_CREATION_NOTICES'])) {
|
||||
$maxConfigCreationNotices = intval($this->environment['BOUNCER_MAXIMUM_NGINX_CONFIG_CREATION_NOTICES']);
|
||||
$originalMaximumNginxConfigCreationNotices = $this->getMaximumNginxConfigCreationNotices();
|
||||
$this->setMaximumNginxConfigCreationNotices($maxConfigCreationNotices);
|
||||
$this->logger->warning(' Maximum Nginx config creation notices has been over-ridden: {original} => {new}', ['emoji' => Emoji::upsideDownFace(), 'original' => $originalMaximumNginxConfigCreationNotices, 'new' => $this->getMaximumNginxConfigCreationNotices()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getMaximumNginxConfigCreationNotices(): int
|
||||
|
|
@ -194,30 +170,45 @@ class Bouncer
|
|||
|
||||
$containers = json_decode($this->docker->request('GET', 'containers/json')->getBody()->getContents(), true);
|
||||
foreach ($containers as $container) {
|
||||
$envs = [];
|
||||
$inspect = json_decode($this->docker->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
|
||||
if (isset($inspect['Config']['Env'])) {
|
||||
foreach ($inspect['Config']['Env'] as $environmentItem) {
|
||||
if (stripos($environmentItem, '=') !== false) {
|
||||
[$envKey, $envVal] = explode('=', $environmentItem, 2);
|
||||
$envs[$envKey] = $envVal;
|
||||
} else {
|
||||
$envs[$environmentItem] = true;
|
||||
}
|
||||
$envs = [];
|
||||
$container = json_decode($this->docker->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
|
||||
if (
|
||||
!isset($container['Config']['Env'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// Parse all the environment variables and store them in an array.
|
||||
foreach ($container['Config']['Env'] as $env) {
|
||||
[$envKey, $envVal] = explode('=', $env, 2);
|
||||
if (str_starts_with($envKey, 'BOUNCER_')) {
|
||||
$envs[$envKey] = $envVal;
|
||||
}
|
||||
}
|
||||
ksort($envs);
|
||||
// If there are no BOUNCER_* environment variables, skip this service.
|
||||
if (count($envs) == 0) {
|
||||
continue;
|
||||
}
|
||||
// If BOUNCER_IGNORE is set, skip this service.
|
||||
if (isset($envs['BOUNCER_IGNORE'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($envs['BOUNCER_DOMAIN'])) {
|
||||
$bouncerTarget = (new Target($this->logger))
|
||||
->setId($inspect['Id'])
|
||||
$bouncerTarget = (new Target(
|
||||
logger: $this->logger,
|
||||
settings: $this->settings,
|
||||
))
|
||||
->setId($container['Id'])
|
||||
;
|
||||
$bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget);
|
||||
|
||||
if (isset($inspect['NetworkSettings']['IPAddress']) && !empty($inspect['NetworkSettings']['IPAddress'])) {
|
||||
if (!empty($container['NetworkSettings']['IPAddress'])) {
|
||||
// As per docker service
|
||||
$bouncerTarget->setEndpointHostnameOrIp($inspect['NetworkSettings']['IPAddress']);
|
||||
$bouncerTarget->setEndpointHostnameOrIp($container['NetworkSettings']['IPAddress']);
|
||||
} else {
|
||||
// As per docker compose
|
||||
$networks = array_values($inspect['NetworkSettings']['Networks']);
|
||||
$networks = array_values($container['NetworkSettings']['Networks']);
|
||||
$bouncerTarget->setEndpointHostnameOrIp($networks[0]['IPAddress']);
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +239,7 @@ class Bouncer
|
|||
$services = json_decode($this->docker->request('GET', 'services')->getBody()->getContents(), true);
|
||||
|
||||
if (isset($services['message'])) {
|
||||
$this->logger->debug('Something happened while interrogating services.. This node is not a swarm node, cannot have services: {message}', ['emoji' => Emoji::warning(), 'message' => $services['message']]);
|
||||
$this->logger->debug('Something happened while interrogating services.. This node is not a swarm node, cannot have services: {message}', ['emoji' => Emoji::warning() . ' ', 'message' => $services['message']]);
|
||||
} else {
|
||||
foreach ($services as $service) {
|
||||
$envs = [];
|
||||
|
|
@ -260,24 +251,48 @@ class Bouncer
|
|||
) {
|
||||
continue;
|
||||
}
|
||||
// Parse all the environment variables and store them in an array.
|
||||
foreach ($service['Spec']['TaskTemplate']['ContainerSpec']['Env'] as $env) {
|
||||
[$eKey, $eVal] = explode('=', $env, 2);
|
||||
$envs[$eKey] = $eVal;
|
||||
[$envKey, $envVal] = explode('=', $env, 2);
|
||||
if (str_starts_with($envKey, 'BOUNCER_')) {
|
||||
$envs[$envKey] = $envVal;
|
||||
}
|
||||
}
|
||||
ksort($envs);
|
||||
// If there are no BOUNCER_* environment variables, skip this service.
|
||||
if (count($envs) == 0) {
|
||||
continue;
|
||||
}
|
||||
// if BOUNCER_IGNORE is set, skip this service.
|
||||
if (isset($envs['BOUNCER_IGNORE'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bouncerTarget = (new Target(
|
||||
logger: $this->logger,
|
||||
settings: $this->settings,
|
||||
));
|
||||
if (isset($envs['BOUNCER_LABEL'])) {
|
||||
$bouncerTarget->setLabel($envs['BOUNCER_LABEL']);
|
||||
}
|
||||
if (isset($envs['BOUNCER_DOMAIN'])) {
|
||||
$bouncerTarget = (new Target($this->logger))
|
||||
->setId($service['ID'])
|
||||
;
|
||||
$bouncerTarget->setId($service['ID']);
|
||||
$bouncerTarget->setLabel($service['Spec']['Name']);
|
||||
$bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget);
|
||||
|
||||
if ($bouncerTarget->isPortSet()) {
|
||||
$bouncerTarget->setEndpointHostnameOrIp($service['Spec']['Name']);
|
||||
// $this->logger->info('Ports for {target_name} has been explicitly set to {host}:{port}.', ['emoji' => Emoji::warning(), 'target_name' => $bouncerTarget->getName(), 'host' => $bouncerTarget->getEndpointHostnameOrIp(), 'port' => $bouncerTarget->getPort()]);
|
||||
// $this->logger->info('{label}: Ports for {target_name} has been explicitly set to {host}:{port}.', ['emoji' => Emoji::warning().' ', 'target_name' => $bouncerTarget->getName(), 'host' => $bouncerTarget->getEndpointHostnameOrIp(), 'port' => $bouncerTarget->getPort()]);
|
||||
} elseif (isset($service['Endpoint']['Ports'])) {
|
||||
$bouncerTarget->setEndpointHostnameOrIp('172.17.0.1');
|
||||
$bouncerTarget->setPort(intval($service['Endpoint']['Ports'][0]['PublishedPort']));
|
||||
} else {
|
||||
$this->logger->warning('Ports block missing for {target_name}.', ['emoji' => Emoji::warning(), 'target_name' => $bouncerTarget->getName()]);
|
||||
$this->logger->warning('{label}: ports block missing for {target_name}. Try setting BOUNCER_TARGET_PORT.', ['emoji' => Emoji::warning() . ' ', 'label' => $bouncerTarget->getLabel(), 'target_name' => $bouncerTarget->getName()]);
|
||||
\Kint::dump(
|
||||
$bouncerTarget->getId(),
|
||||
$bouncerTarget->getLabel(),
|
||||
$envs
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
|
@ -306,18 +321,42 @@ class Bouncer
|
|||
|
||||
public function run(): void
|
||||
{
|
||||
$gitHash = substr($this->environment['GIT_SHA'], 0, 7);
|
||||
$buildDate = Carbon::parse($this->environment['BUILD_DATE']);
|
||||
$gitMessage = trim($this->environment['GIT_COMMIT_MESSAGE']);
|
||||
$this->logger->info(' Starting Bouncer. Built on {build_date}, {build_ago}', ['emoji' => Emoji::redHeart(), 'build_date' => $buildDate->toDateTimeString(), 'build_ago' => $buildDate->ago()]);
|
||||
$this->logger->info(' Build #{git_sha}: "{git_message}"', ['emoji' => Emoji::memo(), 'git_sha' => $gitHash, 'git_message' => $gitMessage]);
|
||||
$this->logger->info('Starting Bouncer. Built {build_id} on {build_date}, {build_ago}', ['emoji' => Emoji::redHeart() . ' ', 'build_id' => $this->settings->get('build/id'), 'build_date' => $this->settings->get('build/date')->toDateTimeString(), 'build_ago' => $this->settings->get('build/date')->ago()]);
|
||||
$this->logger->info('Build #{git_sha}: "{build_message}"', ['emoji' => Emoji::memo(), 'git_sha' => $this->settings->get('build/sha_short'), 'build_message' => $this->settings->get('build/message')]);
|
||||
$this->logger->debug(' > HTTPS Listener is on {https_port}', ['emoji' => Emoji::ship(), 'https_port' => $this->settings->get('bouncer/https_port')]);
|
||||
$this->logger->debug(' > HTTP Listener is on {http_port}', ['emoji' => Emoji::ship(), 'http_port' => $this->settings->get('bouncer/http_port')]);
|
||||
|
||||
// Allow defined global cert if set
|
||||
if ($this->settings->has('ssl/global_cert') && $this->settings->has('ssl/global_cert_key')) {
|
||||
$this->setUseGlobalCert(true);
|
||||
$this->providedCertificateStore->write('global.crt', str_replace('\\n', "\n", trim($this->settings->get('ssl/global_cert'), '"')));
|
||||
$this->providedCertificateStore->write('global.key', str_replace('\\n', "\n", trim($this->settings->get('ssl/global_cert_key'), '"')));
|
||||
}
|
||||
$this->logger->debug(' > Global Cert is {enabled}', ['emoji' => Emoji::globeShowingEuropeAfrica(), 'enabled' => $this->isUseGlobalCert() ? 'enabled' : 'disabled']);
|
||||
|
||||
// Determine forced update interval.
|
||||
if ($this->settings->has('bouncer/forced_update_interval_seconds')) {
|
||||
$this->setForcedUpdateIntervalSeconds($this->settings->get('bouncer/forced_update_interval_seconds'));
|
||||
}
|
||||
$this->logger->debug(' > Forced Update Interval is {state}', ['emoji' => Emoji::watch(), 'state' => $this->getForcedUpdateIntervalSeconds() > 0 ? $this->getForcedUpdateIntervalSeconds() : 'disabled']);
|
||||
|
||||
// Determine maximum notices for nginx config creation.
|
||||
if ($this->settings->has('bouncer/max_nginx_config_creation_notices')) {
|
||||
$maxConfigCreationNotices = $this->settings->get('bouncer/max_nginx_config_creation_notices');
|
||||
$originalMaximumNginxConfigCreationNotices = $this->getMaximumNginxConfigCreationNotices();
|
||||
$this->setMaximumNginxConfigCreationNotices($maxConfigCreationNotices);
|
||||
$this->logger->debug(' > Maximum Nginx config creation notices has been over-ridden: {original} => {new}', ['emoji' => Emoji::hikingBoot(), 'original' => $originalMaximumNginxConfigCreationNotices, 'new' => $this->getMaximumNginxConfigCreationNotices()]);
|
||||
}
|
||||
|
||||
// State if non-SSL is allowed. This is processed in the Target class.
|
||||
$this->logger->debug(' > Allow non-SSL is {enabled}', ['emoji' => Emoji::ship(), 'enabled' => $this->settings->get('ssl/allow_non_ssl') ? 'enabled' : 'disabled']);
|
||||
|
||||
try {
|
||||
$this->stateHasChanged();
|
||||
} catch (ConnectException $connectException) {
|
||||
$this->logger->critical('Could not connect to docker socket! Did you map it?', ['emoji' => Emoji::cryingCat()]);
|
||||
$this->logger->critical('Could not connect to docker socket! Did you forget to map it?', ['emoji' => Emoji::cryingCat()]);
|
||||
|
||||
exit;
|
||||
exit(1);
|
||||
}
|
||||
while (true) {
|
||||
$this->runLoop();
|
||||
|
|
@ -326,64 +365,78 @@ class Bouncer
|
|||
|
||||
public function parseContainerEnvironmentVariables(array $envs, Target $bouncerTarget): Target
|
||||
{
|
||||
foreach ($envs as $eKey => $eVal) {
|
||||
if(empty($eVal)){
|
||||
$this->logger->warning("{key} set for {target} is empty, skipping.", ['key' => $eKey, 'emoji' => Emoji::warning(), 'target' => $bouncerTarget->getName()]);
|
||||
continue;
|
||||
}
|
||||
switch ($eKey) {
|
||||
// Process label and name specifically before all else.
|
||||
foreach (array_filter($envs) as $envKey => $envVal) {
|
||||
switch ($envKey) {
|
||||
case 'BOUNCER_LABEL':
|
||||
$bouncerTarget->setLabel($eVal);
|
||||
$bouncerTarget->setLabel($envVal);
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_DOMAIN':
|
||||
$domains = explode(',', $eVal);
|
||||
$domains = explode(',', $envVal);
|
||||
array_walk($domains, function (&$domain, $key): void {
|
||||
$domain = trim($domain);
|
||||
});
|
||||
$bouncerTarget->setDomains($domains);
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
foreach (array_filter($envs) as $envKey => $envVal) {
|
||||
switch ($envKey) {
|
||||
case 'BOUNCER_AUTH':
|
||||
[$username, $password] = explode(':', $eVal);
|
||||
[$username, $password] = explode(':', $envVal);
|
||||
$bouncerTarget->setAuth($username, $password);
|
||||
// $this->logger->info('{label}: Basic Auth has been enabled.', ['emoji' => Emoji::key(), 'label' => $bouncerTarget->getLabel(),]);
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_HOST_OVERRIDE':
|
||||
$bouncerTarget->setHostOverride($eVal);
|
||||
$bouncerTarget->setHostOverride($envVal);
|
||||
$this->logger->warning('{label}: Host reported to container overridden and set to {host_override}.', ['emoji' => Emoji::hikingBoot() . ' ', 'label' => $bouncerTarget->getLabel(), 'host_override' => $bouncerTarget->getHostOverride()]);
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_LETSENCRYPT':
|
||||
$bouncerTarget->setLetsEncrypt(in_array(strtolower($eVal), ['yes', 'true'], true));
|
||||
$bouncerTarget->setLetsEncrypt(in_array(strtolower($envVal), ['yes', 'true'], true));
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_CERT':
|
||||
$bouncerTarget->setCustomCert($envVal);
|
||||
$this->logger->info('{label}: Custom cert specified', ['emoji' => Emoji::locked(), 'label' => $bouncerTarget->getLabel()]);
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_CERT_KEY':
|
||||
$bouncerTarget->setCustomCertKey($envVal);
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_TARGET_PORT':
|
||||
$bouncerTarget->setPort(intval($eVal));
|
||||
$bouncerTarget->setPort(intval($envVal));
|
||||
// $this->logger->info('{label}: Target port set to {port}.', ['emoji' => Emoji::ship(), 'label' => $bouncerTarget->getLabel(), 'port' => $bouncerTarget->getPort(),]);
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_ALLOW_NON_SSL':
|
||||
$bouncerTarget->setAllowNonSSL(in_array(strtolower($eVal), ['yes', 'true'], true));
|
||||
$bouncerTarget->setAllowNonSSL(in_array(strtolower($envVal), ['yes', 'true'], true));
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_ALLOW_WEBSOCKETS':
|
||||
$bouncerTarget->setAllowWebsocketSupport(in_array(strtolower($eVal), ['yes', 'true'], true));
|
||||
$bouncerTarget->setAllowWebsocketSupport(in_array(strtolower($envVal), ['yes', 'true'], true));
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_ALLOW_LARGE_PAYLOADS':
|
||||
$bouncerTarget->setAllowLargePayloads(in_array(strtolower($eVal), ['yes', 'true'], true));
|
||||
$bouncerTarget->setAllowLargePayloads(in_array(strtolower($envVal), ['yes', 'true'], true));
|
||||
|
||||
break;
|
||||
|
||||
case 'BOUNCER_PROXY_TIMEOUT_SECONDS':
|
||||
$bouncerTarget->setProxyTimeoutSeconds(is_numeric($eVal) ? intval($eVal) : null);
|
||||
$bouncerTarget->setProxyTimeoutSeconds(is_numeric($envVal) ? intval($envVal) : null);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -456,13 +509,13 @@ class Bouncer
|
|||
if ($this->lastUpdateEpoch === null) {
|
||||
$isTainted = true;
|
||||
} elseif ($this->forcedUpdateIntervalSeconds > 0 && $this->lastUpdateEpoch <= time() - $this->forcedUpdateIntervalSeconds) {
|
||||
$this->logger->warning(' Forced update interval of {interval_seconds} seconds has been reached, forcing update.', ['emoji' => Emoji::watch(), 'interval_seconds' => $this->forcedUpdateIntervalSeconds]);
|
||||
$this->logger->warning('Forced update interval of {interval_seconds} seconds has been reached, forcing update.', ['emoji' => Emoji::watch(), 'interval_seconds' => $this->forcedUpdateIntervalSeconds]);
|
||||
$isTainted = true;
|
||||
} elseif ($this->previousContainerState === []) {
|
||||
$this->logger->warning(' Initial state has not been set, forcing update.', ['emoji' => Emoji::watch()]);
|
||||
$this->logger->warning('Initial state has not been set, forcing update.', ['emoji' => Emoji::watch()]);
|
||||
$isTainted = true;
|
||||
} elseif ($this->previousSwarmState === []) {
|
||||
$this->logger->warning(' Initial swarm state has not been set, forcing update.', ['emoji' => Emoji::watch()]);
|
||||
$this->logger->warning('Initial swarm state has not been set, forcing update.', ['emoji' => Emoji::watch()]);
|
||||
$isTainted = true;
|
||||
}
|
||||
|
||||
|
|
@ -493,8 +546,8 @@ class Bouncer
|
|||
// Calculate Container State Hash
|
||||
$containerStateDiff = $this->diff($this->previousContainerState, $newContainerState);
|
||||
if (!$isTainted && !empty($containerStateDiff)) {
|
||||
if($this->settings->if('logger/show_state_deltas')) {
|
||||
$this->logger->warning(' Container state has changed', ['emoji' => Emoji::warning()]);
|
||||
if ($this->settings->if('logger/show_state_deltas')) {
|
||||
$this->logger->warning('Container state has changed', ['emoji' => Emoji::warning() . ' ']);
|
||||
echo $containerStateDiff;
|
||||
}
|
||||
$isTainted = true;
|
||||
|
|
@ -506,7 +559,7 @@ class Bouncer
|
|||
if ($this->isSwarmMode()) {
|
||||
$services = json_decode($this->docker->request('GET', 'services')->getBody()->getContents(), true);
|
||||
if (isset($services['message'])) {
|
||||
$this->logger->warning('Something happened while interrogating services.. This node is not a swarm node, cannot have services: {message}', ['emoji' => Emoji::warning(), 'message' => $services['message']]);
|
||||
$this->logger->warning('Something happened while interrogating services.. This node is not a swarm node, cannot have services: {message}', ['emoji' => Emoji::warning() . ' ', 'message' => $services['message']]);
|
||||
} else {
|
||||
foreach ($services as $service) {
|
||||
$name = $service['Spec']['Name'];
|
||||
|
|
@ -533,8 +586,8 @@ class Bouncer
|
|||
// Calculate Swarm State Hash, if applicable
|
||||
$swarmStateDiff = $this->diff($this->previousSwarmState, $newSwarmState);
|
||||
if ($this->isSwarmMode() && !$isTainted && !empty($swarmStateDiff)) {
|
||||
if($this->settings->if('logger/show_state_deltas')){
|
||||
$this->logger->warning(' Swarm state has changed', ['emoji' => Emoji::warning()]);
|
||||
if ($this->settings->if('logger/show_state_deltas')) {
|
||||
$this->logger->warning('Swarm state has changed', ['emoji' => Emoji::warning() . ' ']);
|
||||
echo $swarmStateDiff;
|
||||
}
|
||||
$isTainted = true;
|
||||
|
|
@ -570,13 +623,13 @@ class Bouncer
|
|||
} catch (ServerException $exception) {
|
||||
$this->setSwarmMode(false);
|
||||
} catch (ConnectException $exception) {
|
||||
$this->logger->critical('Unable to connect to docker socket!', ['emoji' => Emoji::warning()]);
|
||||
$this->logger->critical('Unable to connect to docker socket!', ['emoji' => Emoji::warning() . ' ']);
|
||||
$this->logger->critical($exception->getMessage());
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$this->logger->info('Swarm mode is {enabled}.', ['emoji' => Emoji::honeybee(), 'enabled' => $this->isSwarmMode() ? 'enabled' : 'disabled']);
|
||||
$this->logger->debug(' > Swarm mode is {enabled}.', ['emoji' => Emoji::honeybee(), 'enabled' => $this->isSwarmMode() ? 'enabled' : 'disabled']);
|
||||
|
||||
$targets = array_values(
|
||||
array_merge(
|
||||
|
|
@ -668,7 +721,7 @@ class Bouncer
|
|||
|
||||
private function writeCertificatesToS3(): void
|
||||
{
|
||||
$this->logger->info(' Uploading Certificates to S3', ['emoji' => Emoji::CHARACTER_UP_ARROW]);
|
||||
$this->logger->info('Uploading Certificates to S3', ['emoji' => Emoji::CHARACTER_UP_ARROW]);
|
||||
foreach ($this->certificateStoreLocal->listContents('/archive', true) as $file) {
|
||||
/** @var FileAttributes $file */
|
||||
if ($file->isFile()) {
|
||||
|
|
@ -696,20 +749,12 @@ class Bouncer
|
|||
$changedTargets[strrev($target->getName())] = $target;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @var Target[] $changedTargets
|
||||
*/
|
||||
// @var Target[] $changedTargets
|
||||
ksort($changedTargets);
|
||||
$changedTargets = array_values($changedTargets);
|
||||
|
||||
// @todo MB: it'd be nice if this'd explode the domains and tree-walk them like:
|
||||
// com
|
||||
// |- example
|
||||
// | |- www
|
||||
// | |- api
|
||||
// and so on.
|
||||
|
||||
if (count($changedTargets) <= $this->getMaximumNginxConfigCreationNotices()) {
|
||||
/** @var Target $target */
|
||||
foreach ($changedTargets as $target) {
|
||||
$context = [
|
||||
'label' => $target->getLabel(),
|
||||
|
|
@ -717,20 +762,23 @@ class Bouncer
|
|||
'file' => $target->getNginxConfigFileName(),
|
||||
'config_dir' => Bouncer::FILESYSTEM_CONFIG_DIR,
|
||||
];
|
||||
$this->logger->info('Created {label}', $context + ['emoji' => Emoji::pencil() . " "]);
|
||||
$this->logger->debug(' -> {config_dir}/{file}', $context);
|
||||
$this->logger->debug(' -> {domain}', $context);
|
||||
$this->logger->info('Created {label}', $context + ['emoji' => Emoji::pencil() . ' ']);
|
||||
$this->logger->debug(' -> {config_dir}/{file}', $context + ['emoji' => Emoji::pencil() . ' ']);
|
||||
$this->logger->debug(' -> {domain}', $context + ['emoji' => Emoji::pencil() . ' ']);
|
||||
$this->logger->critical('{label} cert type is {cert_type}', $context + ['emoji' => Emoji::catFace() . ' ', 'cert_type' => $target->getTypeCertInUse()->name]);
|
||||
}
|
||||
} else {
|
||||
$this->logger->info(' More than {num_max} Nginx configs generated.. Too many to show them all!', ['emoji' => Emoji::pencil() . " ", 'num_max' => $this->getMaximumNginxConfigCreationNotices()]);
|
||||
$this->logger->info('More than {num_max} Nginx configs generated.. Too many to show them all!', ['emoji' => Emoji::pencil() . ' ', 'num_max' => $this->getMaximumNginxConfigCreationNotices()]);
|
||||
}
|
||||
$this->logger->info('Updated {num_created} Nginx configs, {num_changed} changed..', ['emoji' => Emoji::pencil() . " ", 'num_created' => count($targets), 'num_changed' => count($changedTargets)]);
|
||||
$this->logger->info('Updated {num_created} Nginx configs, {num_changed} changed..', ['emoji' => Emoji::pencil() . ' ', 'num_created' => count($targets), 'num_changed' => count($changedTargets)]);
|
||||
|
||||
$this->pruneNonExistentConfigs($targets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $targets Target[]
|
||||
*
|
||||
* @throws FilesystemException
|
||||
*/
|
||||
protected function pruneNonExistentConfigs(array $targets): void
|
||||
{
|
||||
|
|
@ -742,12 +790,18 @@ class Bouncer
|
|||
}
|
||||
foreach ($this->configFilesystem->listContents('/') as $file) {
|
||||
if (!in_array($file['path'], $expectedFiles)) {
|
||||
$this->logger->info(' Removing {file}', ['emoji' => Emoji::wastebasket(), 'file' => $file['path']]);
|
||||
$this->logger->info('Removing {file}', ['emoji' => Emoji::wastebasket(), 'file' => $file['path']]);
|
||||
$this->configFilesystem->delete($file['path']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private function generateNginxConfig(Target $target): bool
|
||||
{
|
||||
$configData = $this->twig->render('NginxTemplate.twig', $target->__toArray());
|
||||
|
|
@ -759,6 +813,11 @@ class Bouncer
|
|||
$changed = true;
|
||||
}
|
||||
|
||||
if ($target->isUseCustomCert()) {
|
||||
$this->configFilesystem->write($target->getCustomCertPath(), $target->getCustomCert());
|
||||
$this->configFilesystem->write($target->getCustomCertKeyPath(), $target->getCustomCertKey());
|
||||
}
|
||||
|
||||
if ($target->hasAuth()) {
|
||||
$authFileHash = $this->configFilesystem->fileExists($target->getBasicAuthFileName()) ? $this->configFilesystem->read($target->getBasicAuthHashFileName()) : null;
|
||||
if ($target->getAuthHash() != $authFileHash) {
|
||||
|
|
@ -834,13 +893,13 @@ class Bouncer
|
|||
$command->addFlag('n');
|
||||
$command->addFlag('m', $this->environment['BOUNCER_LETSENCRYPT_EMAIL']);
|
||||
$command->addArgument('agree-tos');
|
||||
$this->logger->info('Generating letsencrypt for {target_name} - {command}', ['emoji' => Emoji::pencil() . " ", 'target_name' => $target->getName(), 'command' => $command->__toString()]);
|
||||
$this->logger->info('Generating letsencrypt for {target_name} - {command}', ['emoji' => Emoji::pencil() . ' ', 'target_name' => $target->getName(), 'command' => $command->__toString()]);
|
||||
$shell->run($command);
|
||||
|
||||
if ($shell->getReturnValue() == 0) {
|
||||
$this->logger->info('Generating successful', ['emoji' => Emoji::partyPopper()]);
|
||||
} else {
|
||||
$this->logger->critical('Generating failed!', ['emoji' => Emoji::warning()]);
|
||||
$this->logger->critical('Generating failed!', ['emoji' => Emoji::warning() . ' ']);
|
||||
}
|
||||
|
||||
// Re-enable nginx tweaks
|
||||
|
|
@ -863,18 +922,7 @@ class Bouncer
|
|||
$shell = new Exec();
|
||||
$command = new CommandBuilder('/usr/sbin/nginx');
|
||||
$command->addFlag('s', 'reload');
|
||||
$this->logger->info('Restarting nginx', ['emoji' => Emoji::timerClock() . " "]);
|
||||
$this->logger->info('Restarting nginx', ['emoji' => Emoji::timerClock() . ' ']);
|
||||
$shell->run($command);
|
||||
}
|
||||
|
||||
private function wipeNginxConfig(): void
|
||||
{
|
||||
$this->logger->debug('Purging existing config files ...', ['emoji' => Emoji::bomb()]);
|
||||
foreach ($this->configFilesystem->listContents('') as $file) {
|
||||
/** @var FileAttributes $file */
|
||||
if ($file->isFile() && $file->path() != 'default.conf' && $file->path() != 'default-ssl.conf') {
|
||||
$this->configFilesystem->delete($file->path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
bouncer/src/EnumCertType.php
Normal file
18
bouncer/src/EnumCertType.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bouncer;
|
||||
|
||||
enum EnumCertType
|
||||
{
|
||||
case NO_CERT;
|
||||
|
||||
case LETSENCRYPT_CERT;
|
||||
|
||||
case TEMPORARY_CERT;
|
||||
|
||||
case GLOBAL_CERT;
|
||||
|
||||
case CUSTOM_CERT;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Bouncer\Settings;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Monolog\Level;
|
||||
|
||||
class Settings implements SettingsInterface
|
||||
|
|
@ -42,6 +43,13 @@ class Settings implements SettingsInterface
|
|||
public function __construct()
|
||||
{
|
||||
$this->settings = [
|
||||
'build' => [
|
||||
'sha' => Settings::getEnvironment('GIT_SHA', 'unknown'),
|
||||
'sha_short' => substr(Settings::getEnvironment('GIT_SHA', 'unknown'), 0, 7),
|
||||
'id' => Settings::getEnvironment('GIT_BUILD_ID', 'unknown'),
|
||||
'date' => Carbon::parse(Settings::getEnvironment('BUILD_DATE')),
|
||||
'message' => trim(Settings::getEnvironment('GIT_COMMIT_MESSAGE', '')),
|
||||
],
|
||||
'logger' => [
|
||||
'name' => Settings::getEnvironment('LOG_NAME', 'bouncer'),
|
||||
'path' => Settings::getEnvironment('LOG_FILE', '/var/log/bouncer/bouncer.log'),
|
||||
|
|
@ -51,8 +59,18 @@ class Settings implements SettingsInterface
|
|||
'coloured_output' => Settings::isEnabled('LOG_COLOUR', true),
|
||||
'show_state_deltas' => Settings::isEnabled('LOG_SHOW_STATE_DELTAS'),
|
||||
],
|
||||
'ssl' => [
|
||||
'allow_non_ssl' => Settings::isEnabled('BOUNCER_ALLOW_NON_SSL', true),
|
||||
'global_cert' => Settings::getEnvironment('GLOBAL_CERT'),
|
||||
'global_cert_key' => Settings::getEnvironment('GLOBAL_CERT_KEY'),
|
||||
],
|
||||
'bouncer' => [
|
||||
'http_port' => intval(Settings::getEnvironment('BOUNCER_HTTP_PORT', 80)),
|
||||
'https_port' => intval(Settings::getEnvironment('BOUNCER_HTTPS_PORT', 443)),
|
||||
'forced_update_interval_seconds' => intval(Settings::getEnvironment('BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS', 0)),
|
||||
'max_nginx_config_creation_notices' => intval(Settings::getEnvironment('BOUNCER_MAXIMUM_NGINX_CONFIG_CREATION_NOTICES', 15)),
|
||||
],
|
||||
];
|
||||
\Kint::dump($this->settings);
|
||||
}
|
||||
|
||||
public function get(string $key = '', mixed $default = null): mixed
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Bouncer;
|
||||
|
||||
use Bouncer\Logger\Logger;
|
||||
use Bouncer\Settings\Settings;
|
||||
use Spatie\Emoji\Emoji;
|
||||
|
||||
class Target
|
||||
|
|
@ -16,9 +17,11 @@ class Target
|
|||
private ?int $port = null;
|
||||
private bool $letsEncrypt = false;
|
||||
private string $targetPath;
|
||||
private bool $allowNonSSL = true;
|
||||
private bool $useTemporaryCert = true;
|
||||
private bool $allowNonSSL;
|
||||
private bool $useTemporaryCert = false;
|
||||
private bool $useGlobalCert = false;
|
||||
private ?string $customCert = null;
|
||||
private ?string $customCertKey = null;
|
||||
private bool $allowWebsocketSupport = true;
|
||||
private bool $allowLargePayloads = false;
|
||||
private ?int $proxyTimeoutSeconds = null;
|
||||
|
|
@ -28,29 +31,41 @@ class Target
|
|||
private ?string $hostOverride = null;
|
||||
|
||||
public function __construct(
|
||||
private Logger $logger
|
||||
private Logger $logger,
|
||||
private Settings $settings,
|
||||
) {
|
||||
$this->allowNonSSL = $this->settings->get('ssl/allow_non_ssl', true);
|
||||
}
|
||||
|
||||
public function __toArray()
|
||||
{
|
||||
if ($this->settings->has('ssl/global_cert') && $this->settings->get('ssl/global_cert') === true) {
|
||||
if ($this->getTypeCertInUse() != EnumCertType::GLOBAL_CERT) {
|
||||
$this->logger->debug('{label} has overridden cert type of {cert_type}', ['emoji' => Emoji::exclamationQuestionMark() . ' ', 'label' => $this->getLabel(), 'cert_type' => $this->getTypeCertInUse()->name]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'label' => $this->getLabel(),
|
||||
'domains' => $this->getDomains(),
|
||||
'letsEncrypt' => $this->isLetsEncrypt(),
|
||||
'targetPath' => $this->getTargetPath(),
|
||||
'useTemporaryCert' => $this->isUseTemporaryCert(),
|
||||
'useGlobalCert' => $this->isUseGlobalCert(),
|
||||
'allowNonSSL' => $this->isAllowNonSSL(),
|
||||
'allowWebsocketSupport' => $this->isAllowWebsocketSupport(),
|
||||
'allowLargePayloads' => $this->isAllowLargePayloads(),
|
||||
'proxyTimeoutSeconds' => $this->getProxyTimeoutSeconds(),
|
||||
'hasAuth' => $this->hasAuth(),
|
||||
'authFile' => $this->getBasicAuthFileName(),
|
||||
'hasHostOverride' => $this->hasHostOverride(),
|
||||
'hostOverride' => $this->getHostOverride(),
|
||||
'portHttp' => $this->settings->get('bouncer/http_port'),
|
||||
'portHttps' => $this->settings->get('bouncer/https_port'),
|
||||
] + [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'label' => $this->getLabel(),
|
||||
'serverName' => $this->getNginxServerName(),
|
||||
'certType' => $this->getTypeCertInUse()->name,
|
||||
'targetPath' => $this->getTargetPath(),
|
||||
'customCertFile' => $this->getCustomCertPath(),
|
||||
'customCertKeyFile' => $this->getCustomCertKeyPath(),
|
||||
'useCustomCert' => $this->isUseCustomCert(),
|
||||
'allowNonSSL' => $this->isAllowNonSSL(),
|
||||
'allowWebsocketSupport' => $this->isAllowWebsocketSupport(),
|
||||
'allowLargePayloads' => $this->isAllowLargePayloads(),
|
||||
'proxyTimeoutSeconds' => $this->getProxyTimeoutSeconds(),
|
||||
'hasAuth' => $this->hasAuth(),
|
||||
'authFile' => $this->getBasicAuthFileName(),
|
||||
'hasHostOverride' => $this->hasHostOverride(),
|
||||
'hostOverride' => $this->getHostOverride(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +113,35 @@ class Target
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomCert(): ?string
|
||||
{
|
||||
return $this->customCert;
|
||||
}
|
||||
|
||||
public function setCustomCert(?string $customCert): Target
|
||||
{
|
||||
$this->customCert = $customCert;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomCertKey(): ?string
|
||||
{
|
||||
return $this->customCertKey;
|
||||
}
|
||||
|
||||
public function setCustomCertKey(?string $customCertKey): Target
|
||||
{
|
||||
$this->customCertKey = $customCertKey;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isUseCustomCert(): bool
|
||||
{
|
||||
return $this->customCert !== null && $this->customCertKey !== null;
|
||||
}
|
||||
|
||||
public function getAuth(): array
|
||||
{
|
||||
return [
|
||||
|
|
@ -136,6 +180,16 @@ class Target
|
|||
return "{$this->getBasicAuthFileName()}.hash";
|
||||
}
|
||||
|
||||
public function getCustomCertPath(): string
|
||||
{
|
||||
return "{$this->getName()}.public.pem";
|
||||
}
|
||||
|
||||
public function getCustomCertKeyPath(): string
|
||||
{
|
||||
return "{$this->getName()}.private.pem";
|
||||
}
|
||||
|
||||
public function getBasicAuthFileData(): string
|
||||
{
|
||||
$output = shell_exec(sprintf('htpasswd -nibB -C10 %s %s', $this->getUsername(), $this->getPassword()));
|
||||
|
|
@ -152,6 +206,8 @@ class Target
|
|||
$this->getNginxConfigFileName(),
|
||||
$this->hasAuth() ? $this->getBasicAuthFileName() : null,
|
||||
$this->hasAuth() ? $this->getBasicAuthHashFileName() : null,
|
||||
$this->isUseCustomCert() ? $this->getCustomCertPath() : null,
|
||||
$this->isUseCustomCert() ? $this->getCustomCertKeyPath() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -186,16 +242,23 @@ class Target
|
|||
|
||||
public function setUseGlobalCert(bool $useGlobalCert): self
|
||||
{
|
||||
// $this->logger->critical('setUseGlobalCert: {useGlobalCert}', ['useGlobalCert' => $useGlobalCert ? 'yes' : 'no']);
|
||||
$this->useGlobalCert = $useGlobalCert;
|
||||
|
||||
// Global cert overrides temporary certs.
|
||||
if ($useGlobalCert) {
|
||||
$this->setUseTemporaryCert(false);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTypeCertInUse(): EnumCertType
|
||||
{
|
||||
return match (true) {
|
||||
$this->isUseCustomCert() => EnumCertType::CUSTOM_CERT,
|
||||
$this->isLetsEncrypt() => EnumCertType::LETSENCRYPT_CERT,
|
||||
$this->isUseTemporaryCert() => EnumCertType::TEMPORARY_CERT,
|
||||
$this->isUseGlobalCert() => EnumCertType::GLOBAL_CERT,
|
||||
default => EnumCertType::NO_CERT,
|
||||
};
|
||||
}
|
||||
|
||||
public function isAllowWebsocketSupport(): bool
|
||||
{
|
||||
return $this->allowWebsocketSupport;
|
||||
|
|
@ -240,6 +303,25 @@ class Target
|
|||
return $this->domains;
|
||||
}
|
||||
|
||||
public function getNginxServerNames(): array
|
||||
{
|
||||
$serverNames = [];
|
||||
foreach ($this->domains as $domain) {
|
||||
if (stripos($domain, '*') !== false) {
|
||||
$serverNames[] = sprintf('~^(.*)%s$', str_replace('*', '', $domain));
|
||||
} else {
|
||||
$serverNames[] = $domain;
|
||||
}
|
||||
}
|
||||
|
||||
return $serverNames;
|
||||
}
|
||||
|
||||
public function getNginxServerName(): string
|
||||
{
|
||||
return implode(' ', $this->getNginxServerNames());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $domains
|
||||
*/
|
||||
|
|
@ -304,12 +386,12 @@ class Target
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
public function getName(): string
|
||||
{
|
||||
return reset($this->domains);
|
||||
return str_replace('*.', '', reset($this->domains));
|
||||
}
|
||||
|
||||
public function getLabel()
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label ?? $this->getName();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
server {
|
||||
{% if allowNonSSL %}
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
listen {{ portHttp }};
|
||||
listen [::]:{{ portHttp }};
|
||||
{% endif %}
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name {{ domains|join(' ') }};
|
||||
listen {{ portHttps }} ssl;
|
||||
listen [::]:{{ portHttps }} ssl;
|
||||
server_name {{ serverName }};
|
||||
access_log /var/log/bouncer/{{ name }}.access.log;
|
||||
error_log /var/log/bouncer/{{ name }}.error.log;
|
||||
|
||||
{% if useTemporaryCert %}
|
||||
{% if certType == 'TEMPORARY_CERT' %}
|
||||
ssl_certificate /certs/example.crt;
|
||||
ssl_certificate_key /certs/example.key;
|
||||
{% elseif useGlobalCert %}
|
||||
{% elseif certType == 'GLOBAL_CERT' %}
|
||||
ssl_certificate /certs/global.crt;
|
||||
ssl_certificate_key /certs/global.key;
|
||||
{% else %}
|
||||
{% elseif certType == 'CUSTOM_CERT' %}
|
||||
ssl_certificate /etc/nginx/sites-enabled/{{ customCertFile }};
|
||||
ssl_certificate_key /etc/nginx/sites-enabled/{{ customCertKeyFile }};
|
||||
{% elseif certType == 'LETSENCRYPT_CERT' %}
|
||||
ssl_certificate /etc/letsencrypt/live/{{ name }}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/{{ name }}/privkey.pem;
|
||||
{% endif %}
|
||||
|
|
@ -60,9 +63,9 @@ server {
|
|||
|
||||
{% if not allowNonSSL %}
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name {{ domains|join(' ') }};
|
||||
listen {{ portHttp }};
|
||||
listen [::]:{{ portHttp }};
|
||||
server_name {{ serverName }};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue