merge in changes from upstream

This commit is contained in:
Greyscale 2023-01-09 15:57:00 +01:00
parent 4c40a8eef1
commit 51a97c0c44
No known key found for this signature in database
GPG key ID: 74BAFF55434DA4B2
4 changed files with 172 additions and 33 deletions

View file

@ -17,11 +17,15 @@ RUN apt-get -qq update && \
> /etc/apt/sources.list.d/nginx-stable.list' && \
# Add nginx key
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C && \
# Update
apt-get -qq update && \
# Install Nginx, Certbot bits and apache2-utils for htpasswd generation
apt-get -yqq install --no-install-recommends \
nginx \
python3-certbot-nginx \
apache2-utils \
&& \
# Cleanup
apt-get remove -yqq \
lsb-core \
cups-common \
@ -44,6 +48,7 @@ COPY Nginx-tweak.conf /etc/nginx/conf.d/tweak.conf
COPY NginxTemplate.twig /app/
# Disable daemonising in nginx
RUN sed -i '1s;^;daemon off\;\n;' /etc/nginx/nginx.conf
RUN sed -i 's|include /etc/nginx/sites-enabled/*|include /etc/nginx/sites-enabled/*.conf|g' /etc/nginx/nginx.conf
COPY bouncer /app
COPY composer.* /app/
COPY public /app/public
@ -59,3 +64,6 @@ COPY ./test/public-web-a /app/public
FROM benzine/php:nginx-8.1 as test-app-b
COPY ./test/public-web-b /app/public
FROM benzine/php:nginx-8.1 as test-app-c
COPY ./test/public-web-c /app/public

View file

@ -3,10 +3,8 @@ server {
listen 80;
listen [::]:80;
{% endif %}
{% if enableSSL %}
listen 443 ssl;
listen [::]:443 ssl;
{% endif %}
server_name {{ domains|join(' ') }};
access_log /var/log/bouncer/{{ name }}.access.log;
error_log /var/log/bouncer/{{ name }}.error.log;
@ -17,7 +15,7 @@ server {
{% elseif useGlobalCert %}
ssl_certificate /certs/global.crt;
ssl_certificate_key /certs/global.key;
{% elseif useLetsEncrypt %}
{% else %}
ssl_certificate /etc/letsencrypt/live/{{ name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ name }}/privkey.pem;
{% endif %}
@ -34,6 +32,11 @@ server {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
{% if hasAuth %}
auth_basic "closed site";
auth_basic_user_file sites-enabled/{{ authFile }};
{% endif %}
{% if allowWebsocketSupport %}
# WebSocket support
proxy_http_version 1.1;
@ -41,6 +44,12 @@ server {
proxy_set_header Connection "upgrade";
{% endif %}
{% if proxyTimeoutSeconds %}
proxy_read_timeout {{ proxyTimeoutSeconds }};
proxy_connect_timeout {{ proxyTimeoutSeconds }};
proxy_send_timeout {{ proxyTimeoutSeconds }};
{% endif %}
proxy_pass {{ targetPath }};
}
}
@ -52,4 +61,4 @@ server {
server_name {{ domains|join(' ') }};
return 301 https://$host$request_uri;
}
{% endif %}
{% endif %}

View file

@ -37,6 +37,7 @@ These environment variables need to be applied to the CONSUMING SERVICE and not
| Key | Example | Behaviour |
|--------------------------------|-------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| BOUNCER_DOMAIN | "a.example.com" | The domain that should be directed to this container |
| BOUNCER_AUTH | "username:password" e.g "root:toor" | Add a HTTP BASIC auth requirement to this hostname. |
| BOUNCER_LETSENCRYPT | Values are "yes" or "true", anything else is false | To enable, or disable Lets Encrypt service 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 |
@ -45,4 +46,4 @@ These environment variables need to be applied to the CONSUMING SERVICE and not
| BOUNCER_MAX_PAYLOADS_MEGABYTES | numbers | Size of max payload to allow, in megabytes. Requires BOUNCER_ALLOW_LARGE_PAYLOADS to be enabled |
## Security considerations
If you're putting this behind access control to the docker socket, it will need access to the /swarm /services and /containers endpoints of the docker api.
If you're putting this behind access control to the docker socket, it will need access to the /swarm /services and /containers endpoints of the docker api.

View file

@ -7,6 +7,8 @@ use AdamBrett\ShellWrapper\Runners\Exec;
use Aws\S3\S3Client;
use Bramus\Monolog\Formatter\ColoredLineFormatter;
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use League\Flysystem\FileAttributes;
use League\Flysystem\Filesystem;
@ -28,10 +30,12 @@ class BouncerTarget
private bool $allowNonSSL = true;
private bool $useTemporaryCert = true;
private bool $useGlobalCert = false;
private bool $useLetsEncrypt = false;
private bool $allowWebsocketSupport = true;
private bool $allowLargePayloads = false;
private int $maxPayloadSizeMegabytes = 256;
private ?int $proxyTimeoutSeconds = null;
private ?string $username = null;
private ?string $password = null;
public function __construct(
private Logger $logger
@ -48,18 +52,88 @@ class BouncerTarget
'targetPath' => $this->getTargetPath(),
'useTemporaryCert' => $this->isUseTemporaryCert(),
'useGlobalCert' => $this->isUseGlobalCert(),
'useLetsEncrypt' => $this->isUseLetsEncrypt(),
'allowNonSSL' => $this->isAllowNonSSL(),
'enableSSL' => $this->isEnableSSL(),
'allowWebsocketSupport' => $this->isAllowWebsocketSupport(),
'allowLargePayloads' => $this->isAllowLargePayloads(),
'maxPayloadSizeMegabytes' => $this->getMaxPayloadSizeMegabytes(),
'proxyTimeoutSeconds' => $this->getProxyTimeoutSeconds(),
'hasAuth' => $this->hasAuth(),
'authFile' => $this->getAuthFileName(),
];
}
public function isEnableSSL(): bool
/**
* @return string|null
*/
public function getUsername(): ?string
{
return $this->isUseLetsEncrypt() || $this->isUseGlobalCert() || $this->isUseLetsEncrypt();
return $this->username;
}
/**
* @param string
* @return BouncerTarget
*/
public function setUsername(string $username): BouncerTarget
{
$this->username = $username;
return $this;
}
/**
* @return string|null
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* @param string $password
* @return BouncerTarget
*/
public function setPassword(string $password): BouncerTarget
{
$this->password = $password;
return $this;
}
public function setAuth(string $username, string $password): BouncerTarget
{
return $this->setUsername($username)->setPassword($password);
}
public function hasAuth() : bool
{
return $this->username != null && $this->password != null;
}
public function getFileName() : string
{
return "{$this->getName()}.conf";
}
public function getAuthFileName() : string
{
return "{$this->getName()}.secret";
}
public function getAuthFileData() : string
{
$output = shell_exec(sprintf("htpasswd -nibB -C10 %s %s",$this->getUsername(), $this->getPassword()));
return trim($output)."\n";
}
public function getProxyTimeoutSeconds(): ?int
{
return $this->proxyTimeoutSeconds;
}
public function setProxyTimeoutSeconds(?int $proxyTimeoutSeconds): BouncerTarget
{
$this->proxyTimeoutSeconds = $proxyTimeoutSeconds;
return $this;
}
public function isUseTemporaryCert(): bool
@ -74,18 +148,6 @@ class BouncerTarget
return $this;
}
public function isUseLetsEncrypt(): bool
{
return $this->useLetsEncrypt;
}
public function setUseLetsEncrypt(bool $useLetsEncrypt): BouncerTarget
{
$this->useLetsEncrypt = $useLetsEncrypt;
return $this;
}
public function isUseGlobalCert(): bool
{
return $this->useGlobalCert;
@ -96,7 +158,9 @@ class BouncerTarget
$this->useGlobalCert = $useGlobalCert;
// Global cert overrides temporary certs.
$this->setUseTemporaryCert(false);
if($useGlobalCert) {
$this->setUseTemporaryCert(false);
}
return $this;
}
@ -277,6 +341,7 @@ class Bouncer
private bool $useGlobalCert = false;
private int $forcedUpdateIntervalSeconds = 600;
private ?int $lastUpdateEpoch = null;
private int $maximumNginxConfigCreationNotices = 15;
public function __construct()
{
@ -346,6 +411,26 @@ class Bouncer
$this->logger->info(sprintf('%s Forced update interval is every %d seconds', Emoji::watch(), $this->getForcedUpdateIntervalSeconds()));
}
/**
* @return int
*/
public function getMaximumNginxConfigCreationNotices(): int
{
return $this->maximumNginxConfigCreationNotices;
}
/**
* @param int $maximumNginxConfigCreationNotices
*
* @return Bouncer
*/
public function setMaximumNginxConfigCreationNotices(int $maximumNginxConfigCreationNotices): Bouncer
{
$this->maximumNginxConfigCreationNotices = $maximumNginxConfigCreationNotices;
return $this;
}
public function isSwarmMode(): bool
{
return $this->swarmMode;
@ -433,7 +518,7 @@ class Bouncer
// $valid ? 'is valid' : 'is not valid'
// ));
if ($valid) {
$bouncerTargets[$bouncerTarget->getDomains()[0]] = $bouncerTarget;
$bouncerTargets[] = $bouncerTarget;
}
}
}
@ -495,7 +580,7 @@ class Bouncer
// $valid ? 'is valid' : 'is not valid'
// ));
if ($valid) {
$bouncerTargets[$bouncerTarget->getDomains()[0]] = $bouncerTarget;
$bouncerTargets[] = $bouncerTarget;
}
}
}
@ -510,7 +595,7 @@ class Bouncer
try {
$this->stateHasChanged();
} catch (\GuzzleHttp\Exception\ConnectException $connectException) {
} catch (ConnectException $connectException) {
$this->logger->critical(sprintf('%s Could not connect to docker socket! Did you map it?', Emoji::CHARACTER_CRYING_CAT));
exit;
@ -533,9 +618,14 @@ class Bouncer
break;
case 'BOUNCER_AUTH':
list($username, $password) = explode(":", $eVal);
$bouncerTarget->setAuth($username, $password);
break;
case 'BOUNCER_LETSENCRYPT':
$bouncerTarget->setLetsEncrypt(in_array(strtolower($eVal), ['yes', 'true'], true));
$bouncerTarget->setUseLetsEncrypt(true);
break;
@ -562,6 +652,11 @@ class Bouncer
case 'BOUNCER_MAX_PAYLOADS_MEGABYTES':
$bouncerTarget->setMaxPayloadSizeMegabytes(is_numeric($eVal) ? $eVal : 256);
break;
case 'BOUNCER_PROXY_TIMEOUT_SECONDS':
$bouncerTarget->setProxyTimeoutSeconds(is_numeric($eVal) ? $eVal : null);
break;
}
}
@ -627,8 +722,13 @@ class Bouncer
try {
$determineSwarmMode = json_decode($this->client->request('GET', 'swarm')->getBody()->getContents(), true);
$this->setSwarmMode(!isset($determineSwarmMode['message']));
} catch (\GuzzleHttp\Exception\ServerException $exception) {
} catch (ServerException $exception) {
$this->setSwarmMode(false);
} catch (ConnectException $exception) {
$this->logger->critical(sprintf('%s Unable to connect to docker socket!', Emoji::warning()));
$this->logger->critical($exception->getMessage());
exit(1);
}
$this->logger->info(sprintf('%s Swarm mode is %s.', Emoji::CHARACTER_HONEYBEE, $this->isSwarmMode() ? 'enabled' : 'disabled'));
@ -643,9 +743,7 @@ class Bouncer
$this->wipeNginxConfig();
$this->logger->info(sprintf('%s Found %d services with BOUNCER_DOMAIN set', Emoji::CHARACTER_MAGNIFYING_GLASS_TILTED_LEFT, count($targets)));
foreach ($targets as $target) {
$this->generateNginxConfig($target);
}
$this->generateNginxConfigs($targets);
$this->generateLetsEncryptCerts($targets);
if ($this->s3Enabled()) {
$this->writeCertificatesToS3();
@ -722,11 +820,34 @@ class Bouncer
}
}
/**
* @param BouncerTarget[] $target
*
* @return $this
*/
private function generateNginxConfigs(array $targets): self
{
foreach ($targets as $target) {
$this->generateNginxConfig($target);
if (count($targets) <= $this->getMaximumNginxConfigCreationNotices()) {
$this->logger->info(sprintf('%s Created Nginx config for http://%s', Emoji::pencil(), $target->getName()));
}
}
if (count($targets) > $this->getMaximumNginxConfigCreationNotices()) {
$this->logger->info(sprintf('%s More than %d Nginx configs generated.. Too many to show them all!', Emoji::pencil(), $this->getMaximumNginxConfigCreationNotices()));
}
$this->logger->info(sprintf('%s Created %d Nginx configs..', Emoji::pencil(), count($targets)));
return $this;
}
private function generateNginxConfig(BouncerTarget $target): self
{
$configData = $this->twig->render('NginxTemplate.twig', $target->__toArray());
$this->configFilesystem->write($target->getName(), $configData);
$this->logger->info(sprintf('%s Created Nginx config for http://%s', Emoji::CHARACTER_PENCIL, $target->getName()));
$this->configFilesystem->write($target->getFileName(), $configData);
if($target->hasAuth()){
$this->configFilesystem->write($target->getAuthFileName(), $target->getAuthFileData());
}
return $this;
}