Merge pull request #164 from WordOps/updating-configuration

Updating configuration
This commit is contained in:
VirtuBox
2019-09-26 16:02:34 +02:00
committed by GitHub
17 changed files with 244 additions and 211 deletions

View File

@@ -8,15 +8,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### v3.9.x - [Unreleased] ### v3.9.x - [Unreleased]
### v3.9.9.1 - 2019-09-26
#### Added
- [SECURE] Allow new ssh port with UFW when running `wo secure --sshport`
- [STACK] Additional Nginx directives to prevent access to log files or backup from web browser
- [CORE] apt-mirror-updater to select the fastest debian/ubuntu mirror with automatic switching between mirrors if the current mirror is being updated
- [SITE] add `--force` to force Let's Encrypt certificate issuance even if DNS check fail
- [STACK] check if another mta is installed before installing sendmail
- [SECURE] `--allowpassword` to allow password when using `--ssh` with `wo secure`
#### Changed
- [SECURE] Improved sshd_config template according to Mozilla Infosec guidelines
- [STACK] Always add stack configuration into Git before making changes to make rollback easier
- [STACK] Render php-fpm pools configuration from template
- [STACK] Adminer updated to v4.7.3
#### Fixed
- [STACK] UFW setup after removing all stacks with `wo stack purge --all`
- [CONFIG] Invalid CORS header
- [STACK] PHP-FPM stack upgrade failure due to pool configuration
### v3.9.9 - 2019-09-24 ### v3.9.9 - 2019-09-24
#### Added #### Added
- [STACK] UFW now available as a stack with flag `--ufw` - [STACK] UFW now available as a stack with flag `--ufw`
- [SECURE] `wo stack secure --ssh` to harden ssh security - [SECURE] `wo secure --ssh` to harden ssh security
- [SECURE] `wo stack secure --sshport` to change ssh port - [SECURE] `wo secure --sshport` to change ssh port
- [SITE] check domain DNS records before issuing a new certificate without DNS API - [SITE] check domain DNS records before issuing a new certificate without DNS API
- [STACK] Acme challenge with DNS Alias mode [acme.sh wiki](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode) - [STACK] Acme challenge with DNS Alias mode `--dnsalias=aliasdomain.tld` [acme.sh wiki](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode)
#### Changed #### Changed

View File

@@ -25,7 +25,7 @@ if not os.path.exists('/var/lib/wo/'):
os.makedirs('/var/lib/wo/') os.makedirs('/var/lib/wo/')
setup(name='wo', setup(name='wo',
version='3.9.9', version='3.9.9.1',
description=long_description, description=long_description,
long_description=long_description, long_description=long_description,
classifiers=[], classifiers=[],
@@ -56,6 +56,7 @@ setup(name='wo',
'SQLAlchemy', 'SQLAlchemy',
'requests', 'requests',
'distro', 'distro',
'apt-mirror-updater',
], ],
data_files=[('/etc/wo', ['config/wo.conf']), data_files=[('/etc/wo', ['config/wo.conf']),
('/etc/wo/plugins.d', conf), ('/etc/wo/plugins.d', conf),

View File

@@ -4,6 +4,7 @@ import os
from cement.core import handler, hook from cement.core import handler, hook
from cement.core.controller import CementBaseController, expose from cement.core.controller import CementBaseController, expose
from wo.core.fileutils import WOFileUtils
from wo.core.git import WOGit from wo.core.git import WOGit
from wo.core.logging import Log from wo.core.logging import Log
from wo.core.random import RANDOM from wo.core.random import RANDOM
@@ -37,6 +38,9 @@ class WOSecureController(CementBaseController):
help='set custom ssh port', action='store_true')), help='set custom ssh port', action='store_true')),
(['--ssh'], dict( (['--ssh'], dict(
help='harden ssh security', action='store_true')), help='harden ssh security', action='store_true')),
(['--allowpassword'], dict(
help='allow password authentification '
'when hardening ssh security', action='store_true')),
(['--force'], (['--force'],
dict(help='force execution without being prompt', dict(help='force execution without being prompt',
action='store_true')), action='store_true')),
@@ -156,7 +160,7 @@ class WOSecureController(CementBaseController):
def secure_ssh(self): def secure_ssh(self):
"""Harden ssh security""" """Harden ssh security"""
pargs = self.app.pargs pargs = self.app.pargs
if not pargs.force: if not pargs.force and not pargs.allowpassword:
start_secure = input('Are you sure you to want to' start_secure = input('Are you sure you to want to'
' harden SSH security ?' ' harden SSH security ?'
'\nSSH login with password will not ' '\nSSH login with password will not '
@@ -165,6 +169,8 @@ class WOSecureController(CementBaseController):
'Harden SSH security [y/N]') 'Harden SSH security [y/N]')
if start_secure != "Y" and start_secure != "y": if start_secure != "Y" and start_secure != "y":
Log.error(self, "Not hardening SSH security") Log.error(self, "Not hardening SSH security")
WOGit.add(self, ["/etc/ssh"],
msg="Adding SSH into Git")
Log.debug(self, "check if /etc/ssh/sshd_config exist") Log.debug(self, "check if /etc/ssh/sshd_config exist")
if os.path.isfile('/etc/ssh/sshd_config'): if os.path.isfile('/etc/ssh/sshd_config'):
Log.debug(self, "looking for the current ssh port") Log.debug(self, "looking for the current ssh port")
@@ -178,7 +184,11 @@ class WOSecureController(CementBaseController):
sudo_user = os.getenv('SUDO_USER') sudo_user = os.getenv('SUDO_USER')
else: else:
sudo_user = '' sudo_user = ''
data = dict(sshport=current_ssh_port, allowpass='no', if pargs.allowpassword:
wo_allowpassword = 'yes'
else:
wo_allowpassword = 'no'
data = dict(sshport=current_ssh_port, allowpass=wo_allowpassword,
user=sudo_user) user=sudo_user)
WOTemplate.deploy(self, '/etc/ssh/sshd_config', WOTemplate.deploy(self, '/etc/ssh/sshd_config',
'sshd.mustache', data) 'sshd.mustache', data)
@@ -213,8 +223,23 @@ class WOSecureController(CementBaseController):
WOShellExec.cmd_exec(self, "sed -i \"s/Port.*/Port " WOShellExec.cmd_exec(self, "sed -i \"s/Port.*/Port "
"{port}/\" /etc/ssh/sshd_config" "{port}/\" /etc/ssh/sshd_config"
.format(port=pargs.user_input)) .format(port=pargs.user_input))
# allow new ssh port if ufw is enabled
if os.path.isfile('/etc/ufw/ufw.conf'):
# add rule for proftpd with UFW
if WOFileUtils.grepcheck(
self, '/etc/ufw/ufw.conf', 'ENABLED=yes'):
try:
WOShellExec.cmd_exec(
self, 'ufw limit {0}'.format(pargs.user_input))
WOShellExec.cmd_exec(
self, 'ufw reload')
except Exception as e:
Log.debug(self, "{0}".format(e))
Log.error(self, "Unable to add UFW rule")
# add ssh into git
WOGit.add(self, ["/etc/ssh"], WOGit.add(self, ["/etc/ssh"],
msg="Adding changed SSH port into Git") msg="Adding changed SSH port into Git")
# restart ssh service
if not WOService.restart_service(self, 'ssh'): if not WOService.restart_service(self, 'ssh'):
Log.error(self, "service SSH restart failed.") Log.error(self, "service SSH restart failed.")
Log.info(self, "Successfully changed SSH port to {port}" Log.info(self, "Successfully changed SSH port to {port}"

View File

@@ -368,6 +368,9 @@ class WOSiteCreateController(CementBaseController):
action='store' or 'store_const', action='store' or 'store_const',
choices=('on', 'subdomain', 'wildcard'), choices=('on', 'subdomain', 'wildcard'),
const='on', nargs='?')), const='on', nargs='?')),
(['--force'],
dict(help="force Let's Encrypt certificate issuance",
action='store_true')),
(['--dns'], (['--dns'],
dict(help="choose dns provider api for letsencrypt", dict(help="choose dns provider api for letsencrypt",
action='store' or 'store_const', action='store' or 'store_const',
@@ -796,9 +799,11 @@ class WOSiteCreateController(CementBaseController):
else: else:
# check DNS records before issuing cert # check DNS records before issuing cert
if not acmedata['dns'] is True: if not acmedata['dns'] is True:
if not WOAcme.check_dns(self, acme_domains): if not pargs.force:
Log.error(self, if not WOAcme.check_dns(self, acme_domains):
"Aborting SSL certificate issuance") Log.error(self,
"Aborting SSL "
"certificate issuance")
Log.debug(self, "Setup Cert with acme.sh for {0}" Log.debug(self, "Setup Cert with acme.sh for {0}"
.format(wo_domain)) .format(wo_domain))
if WOAcme.setupletsencrypt( if WOAcme.setupletsencrypt(
@@ -806,9 +811,10 @@ class WOSiteCreateController(CementBaseController):
WOAcme.deploycert(self, wo_domain) WOAcme.deploycert(self, wo_domain)
else: else:
if not acmedata['dns'] is True: if not acmedata['dns'] is True:
if not WOAcme.check_dns(self, acme_domains): if not pargs.force:
Log.error(self, if not WOAcme.check_dns(self, acme_domains):
"Aborting SSL certificate issuance") Log.error(self,
"Aborting SSL certificate issuance")
if WOAcme.setupletsencrypt( if WOAcme.setupletsencrypt(
self, acme_domains, acmedata): self, acme_domains, acmedata):
WOAcme.deploycert(self, wo_domain) WOAcme.deploycert(self, wo_domain)
@@ -885,6 +891,9 @@ class WOSiteUpdateController(CementBaseController):
choices=('on', 'off', 'renew', 'subdomain', choices=('on', 'off', 'renew', 'subdomain',
'wildcard', 'clean', 'purge'), 'wildcard', 'clean', 'purge'),
const='on', nargs='?')), const='on', nargs='?')),
(['--force'],
dict(help="force LetsEncrypt certificate issuance/renewal",
action='store_true')),
(['--dns'], (['--dns'],
dict(help="choose dns provider api for letsencrypt", dict(help="choose dns provider api for letsencrypt",
action='store' or 'store_const', action='store' or 'store_const',
@@ -901,9 +910,6 @@ class WOSiteUpdateController(CementBaseController):
dict(help="update to proxy site", nargs='+')), dict(help="update to proxy site", nargs='+')),
(['--all'], (['--all'],
dict(help="update all sites", action='store_true')), dict(help="update all sites", action='store_true')),
(['--force'],
dict(help="force letsencrypt certificate renewal",
action='store_true')),
] ]
@expose(help="Update site type or cache") @expose(help="Update site type or cache")
@@ -1446,10 +1452,13 @@ class WOSiteUpdateController(CementBaseController):
else: else:
# check DNS records before issuing cert # check DNS records before issuing cert
if not acmedata['dns'] is True: if not acmedata['dns'] is True:
if not WOAcme.check_dns(self, acme_domains): if not pargs.force:
Log.error( if not WOAcme.check_dns(self,
self, acme_domains):
"Aborting SSL certificate issuance") Log.error(
self,
"Aborting SSL certificate "
"issuance")
Log.debug(self, "Setup Cert with acme.sh for {0}" Log.debug(self, "Setup Cert with acme.sh for {0}"
.format(wo_domain)) .format(wo_domain))
if WOAcme.setupletsencrypt( if WOAcme.setupletsencrypt(
@@ -1460,10 +1469,11 @@ class WOSiteUpdateController(CementBaseController):
else: else:
# check DNS records before issuing cert # check DNS records before issuing cert
if not acmedata['dns'] is True: if not acmedata['dns'] is True:
if not WOAcme.check_dns(self, acme_domains): if not pargs.force:
Log.error( if not WOAcme.check_dns(self, acme_domains):
self, Log.error(
"Aborting SSL certificate issuance") self,
"Aborting SSL certificate issuance")
if WOAcme.setupletsencrypt( if WOAcme.setupletsencrypt(
self, acme_domains, acmedata): self, acme_domains, acmedata):
WOAcme.deploycert(self, wo_domain) WOAcme.deploycert(self, wo_domain)

View File

@@ -930,16 +930,9 @@ def updatewpuserpassword(self, wo_domain, wo_site_webroot):
wo_wp_pass = '' wo_wp_pass = ''
WOFileUtils.chdir(self, '{0}/htdocs/'.format(wo_site_webroot)) WOFileUtils.chdir(self, '{0}/htdocs/'.format(wo_site_webroot))
# Check if wo_domain is wordpress install if not WOShellExec.cmd_exec(self, "wp --allow-root core"
try: " is-installed"):
is_wp = WOShellExec.cmd_exec(self, "wp --allow-root core" # Exit if wo_domain is not wordpress install
" version")
except CommandExecutionError as e:
Log.debug(self, "{0}".format(e))
raise SiteError("is WordPress site? check command failed ")
# Exit if wo_domain is not wordpress install
if not is_wp:
Log.error(self, "{0} does not seem to be a WordPress site" Log.error(self, "{0} does not seem to be a WordPress site"
.format(wo_domain)) .format(wo_domain))
@@ -1333,8 +1326,6 @@ def doCleanupAction(self, domain='', webroot='', dbname='', dbuser='',
if os.path.isfile('/etc/nginx/sites-available/{0}' if os.path.isfile('/etc/nginx/sites-available/{0}'
.format(domain)): .format(domain)):
removeNginxConf(self, domain) removeNginxConf(self, domain)
if os.path.isdir('/etc/letsencrypt/renewal/{0}_ecc'
.format(domain)):
removeAcmeConf(self, domain) removeAcmeConf(self, domain)
if webroot: if webroot:

View File

@@ -129,7 +129,6 @@ class WOStackController(CementBaseController):
pargs.php73 = True pargs.php73 = True
pargs.redis = True pargs.redis = True
pargs.proftpd = True pargs.proftpd = True
pargs.security = True
if pargs.web: if pargs.web:
pargs.nginx = True pargs.nginx = True
@@ -152,7 +151,6 @@ class WOStackController(CementBaseController):
if pargs.security: if pargs.security:
pargs.fail2ban = True pargs.fail2ban = True
pargs.clamav = True pargs.clamav = True
pargs.ufw = True
# Nginx # Nginx
if pargs.nginx: if pargs.nginx:
@@ -261,19 +259,24 @@ class WOStackController(CementBaseController):
# UFW # UFW
if pargs.ufw: if pargs.ufw:
if not WOFileUtils.grep( Log.debug(self, "Setting apt_packages variable for UFW")
self, '/etc/ufw/ufw.conf', 'ENABLED=yes'): apt_packages = apt_packages + ["ufw"]
Log.debug(self, "Setting apt_packages variable for UFW")
apt_packages = apt_packages + ["ufw"]
# sendmail # sendmail
if pargs.sendmail: if pargs.sendmail:
Log.debug(self, "Setting apt_packages variable for Sendmail") Log.debug(self, "Setting apt_packages variable for Sendmail")
if not WOAptGet.is_installed(self, 'sendmail'): if (not WOAptGet.is_installed(self, 'sendmail') and
not WOAptGet.is_installed(self, 'postfix')):
apt_packages = apt_packages + ["sendmail"] apt_packages = apt_packages + ["sendmail"]
else: else:
Log.debug(self, "Sendmail already installed") if WOAptGet.is_installed(self, 'sendmail'):
Log.info(self, "Sendmail already installed") Log.debug(self, "Sendmail already installed")
Log.info(self, "Sendmail already installed")
else:
Log.debug(
self, "Another mta (Postfix) is already installed")
Log.info(
self, "Another mta (Postfix) is already installed")
# proftpd # proftpd
if pargs.proftpd: if pargs.proftpd:
@@ -521,7 +524,6 @@ class WOStackController(CementBaseController):
(not pargs.php73)): (not pargs.php73)):
pargs.web = True pargs.web = True
pargs.admin = True pargs.admin = True
pargs.security = True
if pargs.all: if pargs.all:
pargs.web = True pargs.web = True

View File

@@ -43,7 +43,7 @@ def pre_pref(self, apt_packages):
WORepo.add_key(self, '0xcbcb082a1bb943db', WORepo.add_key(self, '0xcbcb082a1bb943db',
keyserver='keys.gnupg.net') keyserver='keys.gnupg.net')
WORepo.add_key(self, '0xF1656F24C74CD1D8', WORepo.add_key(self, '0xF1656F24C74CD1D8',
keyserver='hkp://keys.gnupg.net') keyserver='keys.gnupg.net')
if "mariadb-server" in apt_packages: if "mariadb-server" in apt_packages:
# generate random 24 characters root password # generate random 24 characters root password
chars = ''.join(random.sample(string.ascii_letters, 24)) chars = ''.join(random.sample(string.ascii_letters, 24))
@@ -153,11 +153,7 @@ def post_pref(self, apt_packages, packages, upgrade=False):
ngxcnf = '/etc/nginx/conf.d' ngxcnf = '/etc/nginx/conf.d'
ngxcom = '/etc/nginx/common' ngxcom = '/etc/nginx/common'
ngxroot = '/var/www/' ngxroot = '/var/www/'
if upgrade: WOGit.add(self, ["/etc/nginx"], msg="Adding Nginx into Git")
if os.path.isdir('/etc/nginx'):
WOGit.add(self,
["/etc/nginx"],
msg="Adding Nginx into Git")
data = dict(tls13=True) data = dict(tls13=True)
WOTemplate.deploy(self, WOTemplate.deploy(self,
'/etc/nginx/nginx.conf', '/etc/nginx/nginx.conf',
@@ -490,6 +486,7 @@ def post_pref(self, apt_packages, packages, upgrade=False):
WOService.restart_service(self, 'nginx') WOService.restart_service(self, 'nginx')
if set(WOVariables.wo_php).issubset(set(apt_packages)): if set(WOVariables.wo_php).issubset(set(apt_packages)):
WOGit.add(self, ["/etc/php"], msg="Adding PHP into Git")
Log.info(self, "Configuring php7.2-fpm") Log.info(self, "Configuring php7.2-fpm")
ngxroot = '/var/www/' ngxroot = '/var/www/'
# Create log directories # Create log directories
@@ -527,72 +524,32 @@ def post_pref(self, apt_packages, packages, upgrade=False):
"/etc/php/7.2/fpm/php.ini") "/etc/php/7.2/fpm/php.ini")
config.write(configfile) config.write(configfile)
# Parse /etc/php/7.2/fpm/php-fpm.conf # Render php-fpm pool template for php7.3
data = dict(pid="/run/php/php7.2-fpm.pid", data = dict(pid="/run/php/php7.2-fpm.pid",
error_log="/var/log/php/7.2/fpm.log", error_log="/var/log/php7.2-fpm.log",
include="/etc/php/7.2/fpm/pool.d/*.conf") include="/etc/php/7.2/fpm/pool.d/*.conf")
Log.debug(self, "writting php7.2 configuration into " WOTemplate.deploy(
"/etc/php/7.2/fpm/php-fpm.conf") self, '/etc/php/7.2/fpm/php-fpm.conf',
wo_php_fpm = open('/etc/php/7.2/fpm/php-fpm.conf', 'php-fpm.mustache', data)
encoding='utf-8', mode='w')
self.app.render((data), 'php-fpm.mustache', out=wo_php_fpm)
wo_php_fpm.close()
if not os.path.isfile('/etc/php/7.2/fpm/pool.d/www.conf.orig'): data = dict(pool='www-php72', listen='php72-fpm.sock',
WOFileUtils.copyfile(self, '/etc/php/7.2/fpm/pool.d/www.conf', user='www-data',
'/etc/php/7.2/fpm/pool.d/www.conf.orig') group='www-data', listenuser='root',
# Parse /etc/php/7.2/fpm/pool.d/www.conf listengroup='www-data', openbasedir=True)
config = configparser.ConfigParser() WOTemplate.deploy(self, '/etc/php/7.2/fpm/pool.d/www.conf',
config.read_file(codecs.open('/etc/php/7.2/fpm/' 'php-pool.mustache', data)
'pool.d/www.conf.orig', data = dict(pool='www-two-php72', listen='php72-two-fpm.sock',
"r", "utf8")) user='www-data',
config['www']['ping.path'] = '/ping' group='www-data', listenuser='root',
config['www']['pm.status_path'] = '/status' listengroup='www-data', openbasedir=True)
config['www']['pm.max_requests'] = '1500' WOTemplate.deploy(self, '/etc/php/7.2/fpm/pool.d/www-two.conf',
config['www']['pm.max_children'] = '50' 'php-pool.mustache', data)
config['www']['pm.start_servers'] = '10'
config['www']['pm.min_spare_servers'] = '5'
config['www']['pm.max_spare_servers'] = '15'
config['www']['request_terminate_timeout'] = '300'
config['www']['pm'] = 'ondemand'
config['www']['chdir'] = '/'
config['www']['prefix'] = '/var/run/php'
config['www']['listen'] = 'php72-fpm.sock'
config['www']['listen.mode'] = '0660'
config['www']['listen.backlog'] = '32768'
config['www']['catch_workers_output'] = 'yes'
with codecs.open('/etc/php/7.2/fpm/pool.d/www.conf',
encoding='utf-8', mode='w') as configfile:
Log.debug(self, "Writing PHP 7.2 configuration into "
"/etc/php/7.2/fpm/pool.d/www.conf")
config.write(configfile)
with open("/etc/php/7.2/fpm/pool.d/www.conf",
encoding='utf-8', mode='a') as myfile:
myfile.write("\nphp_admin_value[open_basedir] "
"= \"/var/www/:/usr/share/php/:"
"/tmp/:/var/run/nginx-cache/:"
"/dev/shm:/dev/urandom\"\n")
# Generate /etc/php/7.2/fpm/pool.d/www-two.conf
WOFileUtils.copyfile(self, "/etc/php/7.2/fpm/pool.d/www.conf",
"/etc/php/7.2/fpm/pool.d/www-two.conf")
WOFileUtils.searchreplace(self, "/etc/php/7.2/fpm/pool.d/"
"www-two.conf", "[www]", "[www-two]")
config = configparser.ConfigParser()
config.read('/etc/php/7.2/fpm/pool.d/www-two.conf')
config['www-two']['listen'] = 'php72-two-fpm.sock'
with open('/etc/php/7.2/fpm/pool.d/www-two.conf',
encoding='utf-8', mode='w') as confifile:
Log.debug(self, "writting PHP7.2 configuration into "
"/etc/php/7.2/fpm/pool.d/www-two.conf")
config.write(confifile)
# Generate /etc/php/7.2/fpm/pool.d/debug.conf # Generate /etc/php/7.2/fpm/pool.d/debug.conf
WOFileUtils.copyfile(self, "/etc/php/7.2/fpm/pool.d/www.conf", WOFileUtils.copyfile(self, "/etc/php/7.2/fpm/pool.d/www.conf",
"/etc/php/7.2/fpm/pool.d/debug.conf") "/etc/php/7.2/fpm/pool.d/debug.conf")
WOFileUtils.searchreplace(self, "/etc/php/7.2/fpm/pool.d/" WOFileUtils.searchreplace(self, "/etc/php/7.2/fpm/pool.d/"
"debug.conf", "[www]", "[debug]") "debug.conf", "[www-php72]", "[debug]")
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read('/etc/php/7.2/fpm/pool.d/debug.conf') config.read('/etc/php/7.2/fpm/pool.d/debug.conf')
config['debug']['listen'] = '127.0.0.1:9172' config['debug']['listen'] = '127.0.0.1:9172'
@@ -663,6 +620,7 @@ def post_pref(self, apt_packages, packages, upgrade=False):
# PHP7.3 configuration # PHP7.3 configuration
if set(WOVariables.wo_php73).issubset(set(apt_packages)): if set(WOVariables.wo_php73).issubset(set(apt_packages)):
WOGit.add(self, ["/etc/php"], msg="Adding PHP into Git")
Log.info(self, "Configuring php7.3-fpm") Log.info(self, "Configuring php7.3-fpm")
ngxroot = '/var/www/' ngxroot = '/var/www/'
# Create log directories # Create log directories
@@ -700,72 +658,32 @@ def post_pref(self, apt_packages, packages, upgrade=False):
"/etc/php/7.3/fpm/php.ini") "/etc/php/7.3/fpm/php.ini")
config.write(configfile) config.write(configfile)
# Parse /etc/php/7.3/fpm/php-fpm.conf # Render php-fpm pool template for php7.3
data = dict(pid="/run/php/php7.3-fpm.pid", data = dict(pid="/run/php/php7.3-fpm.pid",
error_log="/var/log/php7.3-fpm.log", error_log="/var/log/php7.3-fpm.log",
include="/etc/php/7.3/fpm/pool.d/*.conf") include="/etc/php/7.3/fpm/pool.d/*.conf")
Log.debug(self, "writting php 7.3 configuration into " WOTemplate.deploy(
"/etc/php/7.3/fpm/php-fpm.conf") self, '/etc/php/7.3/fpm/php-fpm.conf',
wo_php_fpm = open('/etc/php/7.3/fpm/php-fpm.conf', 'php-fpm.mustache', data)
encoding='utf-8', mode='w')
self.app.render((data), 'php-fpm.mustache', out=wo_php_fpm)
wo_php_fpm.close()
# Parse /etc/php/7.3/fpm/pool.d/www.conf data = dict(pool='www-php73', listen='php73-fpm.sock',
if not os.path.isfile('/etc/php/7.3/fpm/pool.d/www.conf.orig'): user='www-data',
WOFileUtils.copyfile(self, '/etc/php/7.3/fpm/pool.d/www.conf', group='www-data', listenuser='root',
'/etc/php/7.3/fpm/pool.d/www.conf.orig') listengroup='www-data', openbasedir=True)
config = configparser.ConfigParser() WOTemplate.deploy(self, '/etc/php/7.3/fpm/pool.d/www.conf',
config.read_file(codecs.open('/etc/php/7.3/fpm/' 'php-pool.mustache', data)
'pool.d/www.conf.orig', data = dict(pool='www-two-php73', listen='php73-two-fpm.sock',
"r", "utf8")) user='www-data',
config['www']['ping.path'] = '/ping' group='www-data', listenuser='root',
config['www']['pm.status_path'] = '/status' listengroup='www-data', openbasedir=True)
config['www']['pm.max_requests'] = '1500' WOTemplate.deploy(self, '/etc/php/7.3/fpm/pool.d/www-two.conf',
config['www']['pm.max_children'] = '50' 'php-pool.mustache', data)
config['www']['pm.start_servers'] = '10'
config['www']['pm.min_spare_servers'] = '5'
config['www']['pm.max_spare_servers'] = '15'
config['www']['request_terminate_timeout'] = '300'
config['www']['pm'] = 'ondemand'
config['www']['chdir'] = '/'
config['www']['prefix'] = '/var/run/php'
config['www']['listen'] = 'php73-fpm.sock'
config['www']['listen.mode'] = '0660'
config['www']['listen.backlog'] = '32768'
config['www']['catch_workers_output'] = 'yes'
with codecs.open('/etc/php/7.3/fpm/pool.d/www.conf',
encoding='utf-8', mode='w') as configfile:
Log.debug(self, "writting PHP 7.3 configuration into "
"/etc/php/7.3/fpm/pool.d/www.conf")
config.write(configfile)
with open("/etc/php/7.3/fpm/pool.d/www.conf",
encoding='utf-8', mode='a') as myfile:
myfile.write("\nphp_admin_value[open_basedir] "
"= \"/var/www/:/usr/share/php/:"
"/tmp/:/var/run/nginx-cache/:"
"/dev/shm:/dev/urandom\"\n")
# Generate /etc/php/7.3/fpm/pool.d/www-two.conf
WOFileUtils.copyfile(self, "/etc/php/7.3/fpm/pool.d/www.conf",
"/etc/php/7.3/fpm/pool.d/www-two.conf")
WOFileUtils.searchreplace(self, "/etc/php/7.3/fpm/pool.d/"
"www-two.conf", "[www]", "[www-two]")
config = configparser.ConfigParser()
config.read('/etc/php/7.3/fpm/pool.d/www-two.conf')
config['www-two']['listen'] = 'php73-two-fpm.sock'
with open('/etc/php/7.3/fpm/pool.d/www-two.conf',
encoding='utf-8', mode='w') as confifile:
Log.debug(self, "writting PHP7.3 configuration into "
"/etc/php/7.3/fpm/pool.d/www-two.conf")
config.write(confifile)
# Generate /etc/php/7.3/fpm/pool.d/debug.conf # Generate /etc/php/7.3/fpm/pool.d/debug.conf
WOFileUtils.copyfile(self, "/etc/php/7.3/fpm/pool.d/www.conf", WOFileUtils.copyfile(self, "/etc/php/7.3/fpm/pool.d/www.conf",
"/etc/php/7.3/fpm/pool.d/debug.conf") "/etc/php/7.3/fpm/pool.d/debug.conf")
WOFileUtils.searchreplace(self, "/etc/php/7.3/fpm/pool.d/" WOFileUtils.searchreplace(self, "/etc/php/7.3/fpm/pool.d/"
"debug.conf", "[www]", "[debug]") "debug.conf", "[www-php73]", "[debug]")
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read('/etc/php/7.3/fpm/pool.d/debug.conf') config.read('/etc/php/7.3/fpm/pool.d/debug.conf')
config['debug']['listen'] = '127.0.0.1:9173' config['debug']['listen'] = '127.0.0.1:9173'
@@ -836,6 +754,7 @@ def post_pref(self, apt_packages, packages, upgrade=False):
# create mysql config if it doesn't exist # create mysql config if it doesn't exist
if "mariadb-server" in apt_packages: if "mariadb-server" in apt_packages:
WOGit.add(self, ["/etc/mysql"], msg="Adding MySQL into Git")
if not os.path.isfile("/etc/mysql/my.cnf"): if not os.path.isfile("/etc/mysql/my.cnf"):
config = ("[mysqld]\nwait_timeout = 30\n" config = ("[mysqld]\nwait_timeout = 30\n"
"interactive_timeout=60\nperformance_schema = 0" "interactive_timeout=60\nperformance_schema = 0"
@@ -889,6 +808,8 @@ def post_pref(self, apt_packages, packages, upgrade=False):
# create fail2ban configuration files # create fail2ban configuration files
if set(WOVariables.wo_fail2ban).issubset(set(apt_packages)): if set(WOVariables.wo_fail2ban).issubset(set(apt_packages)):
WOGit.add(self, ["/etc/fail2ban"],
msg="Adding Fail2ban into Git")
if not os.path.isfile("/etc/fail2ban/jail.d/custom.conf"): if not os.path.isfile("/etc/fail2ban/jail.d/custom.conf"):
Log.info(self, "Configuring Fail2Ban") Log.info(self, "Configuring Fail2Ban")
data = dict() data = dict()
@@ -914,6 +835,8 @@ def post_pref(self, apt_packages, packages, upgrade=False):
# Proftpd configuration # Proftpd configuration
if "proftpd-basic" in apt_packages: if "proftpd-basic" in apt_packages:
WOGit.add(self, ["/etc/proftpd"],
msg="Adding ProFTPd into Git")
if os.path.isfile("/etc/proftpd/proftpd.conf"): if os.path.isfile("/etc/proftpd/proftpd.conf"):
Log.info(self, "Configuring ProFTPd") Log.info(self, "Configuring ProFTPd")
Log.debug(self, "Setting up Proftpd configuration") Log.debug(self, "Setting up Proftpd configuration")
@@ -936,32 +859,28 @@ def post_pref(self, apt_packages, packages, upgrade=False):
WOFileUtils.chmod(self, "/etc/proftpd/ssl/proftpd.key", 0o700) WOFileUtils.chmod(self, "/etc/proftpd/ssl/proftpd.key", 0o700)
WOFileUtils.chmod(self, "/etc/proftpd/ssl/proftpd.crt", 0o700) WOFileUtils.chmod(self, "/etc/proftpd/ssl/proftpd.crt", 0o700)
data = dict() data = dict()
Log.debug(self, 'Writting the proftpd configuration to ' WOTemplate.deploy(self, '/etc/proftpd/tls.conf',
'file /etc/proftpd/tls.conf') 'proftpd-tls.mustache', data)
wo_proftpdconf = open('/etc/proftpd/tls.conf',
encoding='utf-8', mode='w')
self.app.render((data), 'proftpd-tls.mustache',
out=wo_proftpdconf)
wo_proftpdconf.close()
WOFileUtils.searchreplace(self, "/etc/proftpd/" WOFileUtils.searchreplace(self, "/etc/proftpd/"
"proftpd.conf", "proftpd.conf",
"#Include /etc/proftpd/tls.conf", "#Include /etc/proftpd/tls.conf",
"Include /etc/proftpd/tls.conf") "Include /etc/proftpd/tls.conf")
WOService.restart_service(self, 'proftpd') WOService.restart_service(self, 'proftpd')
# add rule for proftpd with UFW if os.path.isfile('/etc/ufw/ufw.conf'):
if WOFileUtils.grepcheck( # add rule for proftpd with UFW
self, '/etc/ufw/ufw.conf', 'ENABLED=yes'): if WOFileUtils.grepcheck(
try: self, '/etc/ufw/ufw.conf', 'ENABLED=yes'):
WOShellExec.cmd_exec( try:
self, "ufw limit 21") WOShellExec.cmd_exec(
WOShellExec.cmd_exec( self, "ufw limit 21")
self, "ufw allow 49000:50000/tcp") WOShellExec.cmd_exec(
WOShellExec.cmd_exec( self, "ufw allow 49000:50000/tcp")
self, "ufw reload") WOShellExec.cmd_exec(
except CommandExecutionError as e: self, "ufw reload")
Log.debug(self, "{0}".format(e)) except Exception as e:
Log.error(self, "Unable to add UFW rule") Log.debug(self, "{0}".format(e))
Log.error(self, "Unable to add UFW rules")
if ((os.path.isfile("/etc/fail2ban/jail.d/custom.conf")) and if ((os.path.isfile("/etc/fail2ban/jail.d/custom.conf")) and
(not WOFileUtils.grep( (not WOFileUtils.grep(
@@ -1021,6 +940,8 @@ def post_pref(self, apt_packages, packages, upgrade=False):
# set maxmemory 10% for ram below 512MB and 20% for others # set maxmemory 10% for ram below 512MB and 20% for others
# set maxmemory-policy allkeys-lru # set maxmemory-policy allkeys-lru
# enable systemd service # enable systemd service
WOGit.add(self, ["/etc/redis"],
msg="Adding Redis into Git")
Log.debug(self, "Enabling redis systemd service") Log.debug(self, "Enabling redis systemd service")
WOShellExec.cmd_exec(self, "systemctl enable redis-server") WOShellExec.cmd_exec(self, "systemctl enable redis-server")
if (os.path.isfile("/etc/redis/redis.conf") and if (os.path.isfile("/etc/redis/redis.conf") and
@@ -1029,7 +950,7 @@ def post_pref(self, apt_packages, packages, upgrade=False):
Log.wait(self, "Tuning Redis configuration") Log.wait(self, "Tuning Redis configuration")
with open("/etc/redis/redis.conf", with open("/etc/redis/redis.conf",
"a") as redis_file: "a") as redis_file:
redis_file.write("\n# WordOps v3.9.8\n") redis_file.write("\n# WordOps v3.9.9\n")
wo_ram = psutil.virtual_memory().total / (1024 * 1024) wo_ram = psutil.virtual_memory().total / (1024 * 1024)
if wo_ram < 1024: if wo_ram < 1024:
Log.debug(self, "Setting maxmemory variable to " Log.debug(self, "Setting maxmemory variable to "
@@ -1069,8 +990,10 @@ def post_pref(self, apt_packages, packages, upgrade=False):
"tcp-backlog 32768") "tcp-backlog 32768")
WOFileUtils.chown(self, '/etc/redis/redis.conf', WOFileUtils.chown(self, '/etc/redis/redis.conf',
'redis', 'redis', recursive=False) 'redis', 'redis', recursive=False)
WOService.restart_service(self, 'redis-server')
Log.valide(self, "Tuning Redis configuration") Log.valide(self, "Tuning Redis configuration")
WOGit.add(self, ["/etc/redis"],
msg="Adding Redis into Git")
WOService.restart_service(self, 'redis-server')
# ClamAV configuration # ClamAV configuration
if set(WOVariables.wo_clamav).issubset(set(apt_packages)): if set(WOVariables.wo_clamav).issubset(set(apt_packages)):
@@ -1405,13 +1328,10 @@ def post_pref(self, apt_packages, packages, upgrade=False):
Log.debug(self, "configration Anemometer") Log.debug(self, "configration Anemometer")
data = dict(host=WOVariables.wo_mysql_host, port='3306', data = dict(host=WOVariables.wo_mysql_host, port='3306',
user='anemometer', password=chars) user='anemometer', password=chars)
wo_anemometer = open('{0}22222/htdocs/db/anemometer' WOTemplate.deploy(self, '{0}22222/htdocs/db/anemometer'
'/conf/config.inc.php' '/conf/config.inc.php'
.format(WOVariables.wo_webroot), .format(WOVariables.wo_webroot),
encoding='utf-8', mode='w') 'anemometer.mustache', data)
self.app.render((data), 'anemometer.mustache',
out=wo_anemometer)
wo_anemometer.close()
# pt-query-advisor # pt-query-advisor
if any('/usr/bin/pt-query-advisor' == x[1] if any('/usr/bin/pt-query-advisor' == x[1]

View File

@@ -212,7 +212,6 @@ class WOStackUpgradeController(CementBaseController):
if ["php7.3-fpm"] in apt_packages: if ["php7.3-fpm"] in apt_packages:
WOAptGet.remove(self, ['php7.3-fpm'], WOAptGet.remove(self, ['php7.3-fpm'],
auto=False, purge=True) auto=False, purge=True)
# check if nginx upgrade is blocked # check if nginx upgrade is blocked
if os.path.isfile( if os.path.isfile(
'/etc/apt/preferences.d/nginx-block'): '/etc/apt/preferences.d/nginx-block'):

View File

@@ -12,7 +12,7 @@ location @empty_gif {
} }
# Cache static files # Cache static files
location ~* \.(ogg|ogv|svg|svgz|eot|otf|woff|woff2|ttf|m4a|mp4|ttf|rss|atom|jpe?g|gif|cur|heic|png|tiff|ico|webm|mp3|aac|tgz|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|swf|webp|json|webmanifest)$ { location ~* \.(ogg|ogv|svg|svgz|eot|otf|woff|woff2|ttf|m4a|mp4|ttf|rss|atom|jpe?g|gif|cur|heic|png|tiff|ico|webm|mp3|aac|tgz|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|swf|webp|json|webmanifest)$ {
more_set_headers 'Access-Control-Allow-Origin : "*"'; more_set_headers 'Access-Control-Allow-Origin : *';
more_set_headers "Cache-Control : public, no-transform"; more_set_headers "Cache-Control : public, no-transform";
access_log off; access_log off;
log_not_found off; log_not_found off;
@@ -20,7 +20,7 @@ location ~* \.(ogg|ogv|svg|svgz|eot|otf|woff|woff2|ttf|m4a|mp4|ttf|rss|atom|jpe?
} }
# Cache css & js files # Cache css & js files
location ~* \.(?:css(\.map)?|js(\.map)?)$ { location ~* \.(?:css(\.map)?|js(\.map)?)$ {
more_set_headers 'Access-Control-Allow-Origin : "*"'; more_set_headers 'Access-Control-Allow-Origin : *';
more_set_headers "Cache-Control : public, no-transform"; more_set_headers "Cache-Control : public, no-transform";
access_log off; access_log off;
log_not_found off; log_not_found off;

View File

@@ -66,7 +66,7 @@ http {
more_set_headers "X-Frame-Options : SAMEORIGIN"; more_set_headers "X-Frame-Options : SAMEORIGIN";
more_set_headers "X-Xss-Protection : 1; mode=block"; more_set_headers "X-Xss-Protection : 1; mode=block";
more_set_headers "X-Content-Type-Options : nosniff"; more_set_headers "X-Content-Type-Options : nosniff";
more_set_headers "Referrer-Policy : strict-origin-when-cross-origin"; more_set_headers "Referrer-Policy : no-referrer, strict-origin-when-cross-origin";
more_set_headers "X-Download-Options : noopen"; more_set_headers "X-Download-Options : noopen";
# oscp settings # oscp settings

View File

@@ -0,0 +1,23 @@
[{{pool}}]
user = {{user}}
group = {{group}}
listen = {{listen}}
listen.owner = {{listenuser}}
listen.group = {{listengroup}}
pm = ondemand
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
ping.path = /ping
pm.status_path = /status
pm.max_requests = 1500
request_terminate_timeout = 300
chdir = /
prefix = /var/run/php
listen.mode = 0660
listen.backlog = 32768
catch_workers_output = yes
{{#openbasedir}}php_admin_value[open_basedir] = "/var/www/:/usr/share/php/:/tmp/:/var/run/nginx-cache/"{{/openbasedir}}

View File

@@ -33,8 +33,11 @@ X11Forwarding yes
# Allow client to pass locale environment variables # Allow client to pass locale environment variables
AcceptEnv LANG LC_* AcceptEnv LANG LC_*
# override default of no subsystems # LogLevel VERBOSE logs user's key fingerprint on login. Needed to have a clear audit track of which key was using to log in.
Subsystem sftp /usr/lib/openssh/sftp-server LogLevel VERBOSE
# Log sftp level file access (read/write/etc.) that would not be easily logged otherwise.
Subsystem sftp /usr/lib/ssh/sftp-server -f AUTHPRIV -l INFO
# Host keys the client accepts - order here is honored by OpenSSH # Host keys the client accepts - order here is honored by OpenSSH
HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,ssh-rsa,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256 HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,ssh-rsa,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp521,ecdsa-sha2-nistp384,ecdsa-sha2-nistp256
@@ -43,3 +46,7 @@ HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256 KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com
# Use kernel sandbox mechanisms where possible in unprivileged processes
# Systrace on OpenBSD, Seccomp on Linux, seatbelt on MacOSX/Darwin, rlimit elsewhere.
UsePrivilegeSeparation sandbox

View File

@@ -40,15 +40,15 @@ location @robots {
location /wp-content/uploads { location /wp-content/uploads {
location ~ \.(png|jpe?g)$ { location ~ \.(png|jpe?g)$ {
add_header Vary "Accept-Encoding"; add_header Vary "Accept-Encoding";
add_header "Access-Control-Allow-Origin" "*"; more_set_headers 'Access-Control-Allow-Origin : *';
add_header Cache-Control "public, no-transform"; add_header Cache-Control "public, no-transform";
access_log off; access_log off;
log_not_found off; log_not_found off;
expires max; expires max;
try_files $uri$webp_suffix $uri =404; try_files $uri$webp_suffix $uri =404;
} }
location ~ \.php$ { location ~* \.(php|gz|log|zip|tar|rar)$ {
#Prevent Direct Access Of PHP Files From Web Browsers #Prevent Direct Access Of PHP Files & BackupsFrom Web Browsers
deny all; deny all;
} }
} }
@@ -56,7 +56,7 @@ location /wp-content/uploads {
location /wp-content/plugins/ewww-image-optimizer/images { location /wp-content/plugins/ewww-image-optimizer/images {
location ~ \.(png|jpe?g)$ { location ~ \.(png|jpe?g)$ {
add_header Vary "Accept-Encoding"; add_header Vary "Accept-Encoding";
add_header "Access-Control-Allow-Origin" "*"; more_set_headers 'Access-Control-Allow-Origin : *';
add_header Cache-Control "public, no-transform"; add_header Cache-Control "public, no-transform";
access_log off; access_log off;
log_not_found off; log_not_found off;
@@ -72,7 +72,7 @@ location /wp-content/plugins/ewww-image-optimizer/images {
location /wp-content/cache { location /wp-content/cache {
# Cache css & js files # Cache css & js files
location ~* \.(?:css(\.map)?|js(\.map)?|.html)$ { location ~* \.(?:css(\.map)?|js(\.map)?|.html)$ {
add_header "Access-Control-Allow-Origin" "*"; more_set_headers 'Access-Control-Allow-Origin : *';
access_log off; access_log off;
log_not_found off; log_not_found off;
expires 30d; expires 30d;

View File

@@ -18,7 +18,7 @@ class WOAptGet():
""" """
try: try:
with open('/var/log/wo/wordops.log', 'a') as f: with open('/var/log/wo/wordops.log', 'a') as f:
proc = subprocess.Popen('apt-get update', proc = subprocess.Popen('apt-mirror-updater -u',
shell=True, shell=True,
stdin=None, stdout=f, stdin=None, stdout=f,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,

View File

@@ -202,6 +202,37 @@ class WOFileUtils():
Log.debug(self, "{0}".format(e.strerror)) Log.debug(self, "{0}".format(e.strerror))
Log.error(self, "Unable to change owner : {0}".format(path)) Log.error(self, "Unable to change owner : {0}".format(path))
def wpperm(self, path, harden=False):
"""
Fix WordPress site permissions
path : WordPress site path
harden : set 750/640 instead of 755/644
"""
userid = pwd.getpwnam('www-data')[2]
groupid = pwd.getpwnam('www-data')[3]
try:
Log.debug(self, "Fixing WordPress permissions of {0}"
.format(path))
if harden:
dperm = '0o750'
fperm = '0o640'
else:
dperm = '0o755'
fperm = '0o644'
for root, dirs, files in os.walk(path):
for d in dirs:
os.chown(os.path.join(root, d), userid,
groupid)
os.chmod(os.path.join(root, d), dperm)
for f in files:
os.chown(os.path.join(root, d), userid,
groupid)
os.chmod(os.path.join(root, f), fperm)
except OSError as e:
Log.debug(self, "{0}".format(e.strerror))
Log.error(self, "Unable to change owner : {0}".format(path))
def mkdir(self, path): def mkdir(self, path):
""" """
create directories. create directories.

View File

@@ -74,7 +74,7 @@ class WOGit:
try: try:
Log.debug(self, "WOGit: git reset HEAD~ at {0}" Log.debug(self, "WOGit: git reset HEAD~ at {0}"
.format(path)) .format(path))
git.reset("--hard HEAD~") git.reset("HEAD~", "--hard")
except ErrorReturnCode as e: except ErrorReturnCode as e:
Log.debug(self, "{0}".format(e)) Log.debug(self, "{0}".format(e))
Log.error(self, "Unable to git reset at {0} " Log.error(self, "Unable to git reset at {0} "

View File

@@ -11,10 +11,10 @@ class WOVariables():
"""Intialization of core variables""" """Intialization of core variables"""
# WordOps version # WordOps version
wo_version = "3.9.9" wo_version = "3.9.9.1"
# WordOps packages versions # WordOps packages versions
wo_wp_cli = "2.3.0" wo_wp_cli = "2.3.0"
wo_adminer = "4.7.2" wo_adminer = "4.7.3"
wo_phpmyadmin = "4.9.1" wo_phpmyadmin = "4.9.1"
wo_extplorer = "2.1.13" wo_extplorer = "2.1.13"
wo_dashboard = "1.2" wo_dashboard = "1.2"