diff --git a/.gitignore b/.gitignore index 85602c34..5db0ca7f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,11 @@ #temp folder /tmp/ +# runtime data for Docker deployment +/docker/etc/ +/docker/home/ +/docker/root/ + #access /access/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..95289486 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM almalinux:9 + +RUN dnf -y install epel-release \ + && dnf -y install \ + openssh-server nginx supervisor python3 python3-pip cronie screen certbot \ + && dnf clean all + +RUN mkdir -p /var/run/sshd /var/log/nginx /var/cld /root/sbin + +COPY requirements.txt /tmp/requirements.txt +RUN pip3 install --no-cache-dir -r /tmp/requirements.txt && rm -f /tmp/requirements.txt + +COPY supervisord.conf /etc/supervisord.conf +COPY docker/nginx/cld.conf /etc/nginx/conf.d/cld.conf +COPY docker/nginx/sslgen.conf /etc/nginx/sslgen.conf +RUN touch /etc/nginx/accesslist /etc/nginx/deny.conf + +COPY docker/scripts/lets_auto_sign /root/sbin/lets_auto_sign +COPY docker/scripts/lets_renew /root/sbin/lets_renew +COPY docker/scripts/lets_clean_subdomains /root/sbin/lets_clean_subdomains +COPY docker/scripts/lets_auto_gen /etc/cron.d/lets_auto_gen +COPY docker/init_creds.sh /docker/init_creds.sh +RUN chmod 700 /root/sbin/lets_auto_sign /root/sbin/lets_renew /root/sbin/lets_clean_subdomains \ + && chmod 644 /etc/cron.d/lets_auto_gen + +COPY docker/entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] +EXPOSE 22 80 443 +VOLUME ["/var/cld"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/README.md b/README.md index 9588cc92..f0bd2fc6 100644 --- a/README.md +++ b/README.md @@ -521,4 +521,32 @@ bash -x <(wget -qO- "https://raw.githubusercontent.com/classicdevops/cld/master/ During the installation process, all init scripts of the system and modules will be executed, for each of them in interactive mode, you will need to specify the initialization data necessary for the operation of the system and modules An example input will be provided for each type of data requested -Upon completion of the installation, a `password` for the `admin` user and a `link` to the `web interface` will be provided in console. \ No newline at end of file +Upon completion of the installation, a `password` for the `admin` user and a `link` to the `web interface` will be provided in console. +### Running via Docker +1. Build the image and export default credentials using the helper script: + ```bash + sudo ./docker/docker_init.sh /var/cld + ``` + This exports the container's `/etc`, `/home`, and `/root` into `/var/cld/docker`. + A default credentials file is created at `/var/cld/creds/creds_static` with a + symlink `/var/cld/creds/creds` pointing to it. +2. Start the container using docker compose: +```bash +docker-compose up -d +``` +The compose file mounts `/var/cld` to `/var/cld`, `/var/cld/docker/etc` to `/etc`, +`/var/cld/docker/home` to `/home`, and `/var/cld/docker/root` to `/root`. +Ports `22`, `80` and `443` are exposed. The image runs sshd, nginx, cron and all +CLD services under Supervisor. Let’s Encrypt helper scripts placed under +`/root/sbin` handle automatic certificate generation for detected domains. + +You can provide initial credentials through environment variables when running +the container. Any variable with the `CLD_CFG_` prefix will be written to +`/var/cld/creds/creds_env` and used instead of the default +`creds_static` file. For example: + +```yaml +environment: + - CLD_CFG_CLD_DOMAIN=cld.example.com + - CLD_CFG_ADMIN_PASSWORD=secret +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2f24c5d2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + cld: + build: . + volumes: + - /var/cld:/var/cld + - /var/cld/docker/etc:/etc + - /var/cld/docker/home:/home + - /var/cld/docker/root:/root + environment: + # Example credentials passed via environment variables + - CLD_CFG_CLD_DOMAIN=cld.example.com + # CLD_CFG_* variables will be written to /var/cld/creds/creds_env + ports: + - "22:22" + - "80:80" + - "443:443" diff --git a/docker/docker_init.sh b/docker/docker_init.sh new file mode 100755 index 00000000..ec3af6f9 --- /dev/null +++ b/docker/docker_init.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Build the CLD image and extract default credentials. +set -e +IMAGE_NAME=cld-image +DATA_DIR=${1:-/var/cld} + +docker build -t $IMAGE_NAME . +mkdir -p "$DATA_DIR" +# Use the container to populate default credentials and home dirs +# into the target directory. +docker run --rm -v "$DATA_DIR":/var/cld $IMAGE_NAME bash -c "/docker/init_creds.sh /var/cld" + +echo "Credentials prepared under $DATA_DIR" + diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..c8110fbe --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e +mkdir -p /var/cld/creds + +# If an existing creds file is present from a non-containerized setup, convert +# it into creds_static so future runs use symlinks. +if [ -f /var/cld/creds/creds ] && [ ! -L /var/cld/creds/creds ]; then + [ -f /var/cld/creds/creds_static ] || mv /var/cld/creds/creds /var/cld/creds/creds_static +fi + +# If environment variables prefixed with CLD_CFG_ are provided, +# write them into creds_env and use it. Otherwise fall back to creds_static. +if env | grep -q '^CLD_CFG_' ; then + CREDS_ENV=/var/cld/creds/creds_env + : > "$CREDS_ENV" + for VAR in $(env | grep '^CLD_CFG_' | sort); do + KEY=${VAR%%=*} + VALUE=${VAR#*=} + KEY=${KEY#CLD_CFG_} + echo "${KEY}=${VALUE}" >> "$CREDS_ENV" + done + ln -sf creds_env /var/cld/creds/creds +else + # Ensure a static credentials file exists. If a plain creds file exists from + # a previous setup, move it so future runs use the symlink. + if [ -f /var/cld/creds/creds ] && [ ! -L /var/cld/creds/creds ]; then + [ -f /var/cld/creds/creds_static ] || mv /var/cld/creds/creds /var/cld/creds/creds_static + fi + [ -f /var/cld/creds/creds_static ] || touch /var/cld/creds/creds_static + ln -sf creds_static /var/cld/creds/creds +fi + +exec /usr/bin/supervisord -c /etc/supervisord.conf diff --git a/docker/init_creds.sh b/docker/init_creds.sh new file mode 100755 index 00000000..d46b1fa7 --- /dev/null +++ b/docker/init_creds.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Copy container credentials and home directories to the provided directory. +# Usage: ./docker/init_creds.sh [TARGET_DIR] +set -e +TARGET=${1:-/var/cld} + +mkdir -p "$TARGET/docker/etc" "$TARGET/docker/home" "$TARGET/docker/root" "$TARGET/creds" + +cp -a /etc/. "$TARGET/docker/etc/" +cp -a /home/. "$TARGET/docker/home/" 2>/dev/null || true +cp -a /root/. "$TARGET/docker/root/" 2>/dev/null || true + +# prepare default credentials file +touch "$TARGET/creds/creds_static" +ln -sf creds_static "$TARGET/creds/creds" + +echo "Credentials prepared under $TARGET/docker" diff --git a/docker/nginx/cld.conf b/docker/nginx/cld.conf new file mode 100644 index 00000000..d0211b1b --- /dev/null +++ b/docker/nginx/cld.conf @@ -0,0 +1,82 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} +map $http_x_forwarded_proto $real_scheme { + default $http_x_forwarded_proto; + '' $scheme; +} +upstream cldweb { + server 127.0.0.1:8080; +} +upstream cldapi { + server 127.0.0.1:8085; +} +limit_req_zone $binary_remote_addr zone=apiall:10m rate=1r/s; +server { + listen 80; + include sslgen.conf; + access_log /var/log/nginx-main-access.log; + server_name ${CLD_DOMAIN}; + + add_header X-Content-Type-Options nosniff; + + location / { + if ($real_scheme = http) { + return 301 https://$host$request_uri; + } + include accesslist; + deny all; + proxy_pass http://cldweb; + proxy_redirect off; + proxy_buffering off; + proxy_request_buffering off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $real_scheme; + } + + location /api { + if ($real_scheme = http) { + return 301 https://$host$request_uri; + } + include accesslist; + deny all; + rewrite ^/api(.*)$ $1 break; + proxy_pass http://cldapi; + proxy_redirect off; + proxy_buffering off; + proxy_request_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $real_scheme; + } + + location /api/all { + include /etc/nginx/deny.conf; + if ($real_scheme = http) { + return 301 https://$host$request_uri; + } + add_header Access-Control-Allow-Origin *; + limit_req zone=apiall burst=5 nodelay; + rewrite ^/api(.*)$ $1 break; + proxy_pass http://cldapi; + proxy_redirect off; + proxy_buffering off; + proxy_request_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $real_scheme; + } + + location /documentation { + if ($real_scheme = http) { + return 301 https://$host$request_uri; + } + limit_req zone=apiall burst=5 nodelay; + alias /var/www/cld/doc/; + } +} diff --git a/docker/nginx/sslgen.conf b/docker/nginx/sslgen.conf new file mode 100644 index 00000000..a4eb055e --- /dev/null +++ b/docker/nginx/sslgen.conf @@ -0,0 +1,9 @@ +listen *:443 ssl http2; +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_certificate /etc/letsencrypt/live/$ssl_server_name/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/$ssl_server_name/privkey.pem; +location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + alias /usr/share/nginx/html/.well-known/acme-challenge/; +} +error_log /var/log/nginx-ssl-error.log; diff --git a/docker/scripts/lets_auto_gen b/docker/scripts/lets_auto_gen new file mode 100644 index 00000000..e6c7c5f5 --- /dev/null +++ b/docker/scripts/lets_auto_gen @@ -0,0 +1,3 @@ +MAILTO="" +* * * * * root /root/sbin/lets_auto_sign +0 14 * * 1 root /root/sbin/lets_renew diff --git a/docker/scripts/lets_auto_sign b/docker/scripts/lets_auto_sign new file mode 100755 index 00000000..2cf214b1 --- /dev/null +++ b/docker/scripts/lets_auto_sign @@ -0,0 +1,11 @@ +#!/bin/bash +if ps axfu | grep -v grep | grep -q "certonly"; then + echo There is already exist running letsencrypt instance + exit 1 +else + for DOMAIN in $(grep 'cannot load certificate' /var/log/nginx-ssl-error.log | cut -d '"' -f 2 | cut -d / -f 5 | egrep "^[0-9a-z-]+\.([0-9a-z-]+\.)?[a-z]+$" | sort -u) + do + letsencrypt certonly -a webroot -n -m certbot@cldcloud.com --agree-tos --webroot-path=/usr/share/nginx/html -d ${DOMAIN} ; chmod 755 /etc/letsencrypt /etc/letsencrypt/{live,archive} ; chmod -R 755 /etc/letsencrypt/live/$DOMAIN ; chmod -R 755 /etc/letsencrypt/archive/$DOMAIN + done + truncate -s 0 /var/log/nginx-ssl-error.log +fi diff --git a/docker/scripts/lets_clean_subdomains b/docker/scripts/lets_clean_subdomains new file mode 100755 index 00000000..0ff89d72 --- /dev/null +++ b/docker/scripts/lets_clean_subdomains @@ -0,0 +1,6 @@ +#!/bin/bash +for DOMAIN in $(ls /etc/letsencrypt/live/ | egrep "[a-z0-9-]+\.[a-z0-9-]+\.[a-z0-9-]+") +do + echo "${DOMAIN}" + find /etc/letsencrypt/ -name "${DOMAIN}*" | egrep -o "/etc/letsencrypt/.*" | xargs -d "\n" -I {} rm -rf "{}" +done diff --git a/docker/scripts/lets_renew b/docker/scripts/lets_renew new file mode 100755 index 00000000..7e4ce9ba --- /dev/null +++ b/docker/scripts/lets_renew @@ -0,0 +1,27 @@ +#!/bin/bash +check_ssl() +{ + expr \( `echo|timeout 5s openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null| timeout 5s openssl x509 -noout -enddate|cut -d'=' -f2|xargs -I ^ date +%s -d "^"` - `date +%s` \) / 24 / 3600 +} + +truncate -s 0 /tmp/ssl_domains +for DOMAIN in $(ls /etc/letsencrypt/live/ | grep -v README) +do + echo Check $DOMAIN + unset DAYS_LEFT + DAYS_LEFT=$(check_ssl 2>/dev/null) + [ "$DAYS_LEFT" ] || { echo Certificate not defined for $DOMAIN - skip ; continue ; } + echo ${DAYS_LEFT}_$DOMAIN >> /tmp/ssl_domains + done + +cat /tmp/ssl_domains + +for EXDOMAIN in $(cat /tmp/ssl_domains | uniq) +do + EXDAYS=$(echo $EXDOMAIN | cut -d _ -f 1) + DOMAIN=$(echo $EXDOMAIN | cut -d _ -f 2) + if [ "$EXDAYS" -lt "30" ] + then + letsencrypt certonly -a webroot -n -m certbot@cldcloud.com --agree-tos --webroot-path=/usr/share/nginx/html -d ${DOMAIN} ; chmod -R 755 /etc/letsencrypt + fi +done diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e3e19a22 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +cryptography==36.0.1 +Flask==1.1.2 +Flask-Session==0.3.2 +Flask-SocketIO==5.0.1 +itsdangerous==1.1.0 +Jinja2==2.10.1 +lxml==4.6.5 +MarkupSafe==0.23 +PySocks==1.7.1 +pyTelegramBotAPI==4.14.0 +python-dateutil==2.8.1 +python-engineio==4.11.2 +python-linux-procfs==0.7.3 +python-pam==2.0.2 +python-socketio==5.2.1 +urllib3==1.26.5 +Werkzeug==1.0.1 +wsproto==1.2.0 diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 00000000..2514e077 --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,60 @@ +[supervisord] +nodaemon=false + +[program:sshd] +command=/usr/sbin/sshd -D +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr + +[program:nginx] +command=/usr/sbin/nginx -g 'daemon off;' +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr + +[program:cld_web] +command=python3 /var/cld/web/dashboard.py +directory=/var/cld/web +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +environment=PYTHONUNBUFFERED=1 + +[program:cld_api] +command=python3 /var/cld/api/api.py +directory=/var/cld/api +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +environment=PYTHONUNBUFFERED=1 + +[program:cld_auditor] +command=/var/cld/bin/cld-auditor +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +environment=PYTHONUNBUFFERED=1 + +[program:tgbot] +command=python3 /var/cld/bot/telegram/tgbot.py +directory=/var/cld/bot/telegram +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +environment=PYTHONUNBUFFERED=1 + +[program:dcbot] +command=python3 /var/cld/bot/discord/dcbot.py +directory=/var/cld/bot/discord +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +environment=PYTHONUNBUFFERED=1 + +[program:cron] +command=/usr/sbin/crond -n +autorestart=true +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +