Posts Tagged ‘script’

Mass Virtual Hosting Part Eight: MySQL-Proxy for Easy Network Topology Changes and Localhost vs. Sockets

Once your hosting clients are all settled in you may find one day that you need to change their MySQL server address or other configuration parameters. Naturally it’s not going to look good on you or be a very good use of your time to contact every webmaster and have them update their settings. Worse, juggling two active database servers would be a nightmare. Fortunately Oracle came up with mysql-proxy, a lightweight app that sits between your clients and MySQL server(s) which acts as a drop-in replacement for mysqld. Users connect to the proxy like they would the actual server and it transports data to and fro. You can do all sorts of neat things to the data while it’s in motion with lua scripts but that goes beyond the focus of this article.

By default mysql-proxy listens on port tcp/4040 and mysqld listens on tcp/3306. In my experience most users who come from other ISPs are already wired to use localhost as their default SQL host and I don’t want to make them have to remember the port number, which is typically defaulted in most webapps. If you’re running mysqld on the same host you’re serving web from you’ll need to change the port it listens on in /etc/mysql/my.cnf.

MySQL has a contentious age-old convention of changing “localhost” to “use the local socket” rather than resolving localhost to 127.0.0.1 and connecting via TCP. That’s because at the time the decision was made local sockets were far more efficient than using TCP, now it’s not so much an issue. We need to configure mysql-proxy to listen to a socket too or our users will have to use the numeric address, lest they encounter this error:

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

This can be specified – not all too intuitively – by replacing the proxy-address value in /etc/mysql/mysql-proxy.cnf with the default path to the local socket, thus:

proxy-address = /var/run/mysqld/mysqld.sock

Configure the proxy-backend-address variable to reflect the actual server’s location and port number. Restart mysql-proxy and you now have a working, default-looking configuration that can be redirected anywhere. Thanks to the lua capability of the proxy you can even implement fast and easy load balancing and failover, but that will be the topic of another article!

Securing /tmp the Cheap Hooker Way

Numerous exploits take advantage of /tmp’s open nature by uploading scripts or executables to it, then running them from it. While we may or may not be able to do something about the scripts being uploaded (not really possible in a shared hosting environment where any number of unknown clients’ scripts and so on have to be run regardless of their flaws) we can certainly keep them from being run from within /tmp. The noexec mount option makes it impossible to run shell scripts or binaries directly from a filesystem that has been mounted with it. The nosuid option keeps people from using or setting the setuid flag on any files.

If you’re not using LVM or you’re working with virtual machines tossing on another partition might not be practical. Fortunately /tmp works just as well when mounted from a partition image file. Follow through these shell commands, replacing the count= value for dd to whatever size you would like to make the /tmp image in megs. You won’t need much space, on a typical shared hosting server the most one tends to find in there is session data. You are welcome to make a sparse file image however the frequency of writes and deletes as well as the small required size makes this less practical than it might sound. I think 256 megs is a nice round number, unless you’re doing something fancy that actually needs /tmp files.

# dd if=/dev/zero of=/tmp.img bs=1M count=256
# mke2fs -j /tmp.img
# chmod 600 /tmp.img
# mkdir /mnt/tmp
# mount -o loop /tmp.img /mnt/tmp
# mv /tmp/* /mnt/tmp/
# umount /mnt/tmp
# rmdir /mnt/tmp
# mount -o loop,noexec,nosuid /tmp.img /tmp
# chmod 777 /tmp
# chmod +t /tmp

Now we can add the image to our /etc/fstab to ensure that it automounts on boot:

/tmp.img                /tmp            ext3            loop,nosuid,noexec      1 2

Mass Virtual Hosting Part Six: (Remote) Apache Vhost Configuration and Privileged Command Execution with Database-Backed Upkeep System

What a mouthful.

The more I worked with mod_vdbh the more I realized it’s not for me. Neither is its more advanced fork, mod_vhs which adds some useful extensions. While database-backed configuration is an elegant solution to mass virtual hosting it falls short in that it lacks these key (for my purposes) abilities or requires hackery that violates the Keep It Simple, Stupid rule

  • Apache configuration can not be customized on a per-vhost basis, requiring the use of generated .htaccess files for tricks that might be implemented in a user-friendly way via a front-end, such as custom error pages.
  • Additional Directory directives (i.e. those that don’t match the wildcard directives for whatever reason) need to be put into flat files anyway.
  • Logs for individual sites must be split from a common log by a resource-consuming external solution
  • ScriptAlias directive (mod_vhs) only works for one global directory for all sites or not at all (mod_vdbh)
  • These modules are unmaintained, if something better comes out there is the whole hassle of migration to contend with.
  • A new version of apache may break old modules, but flat files will always be supported.

This is a serious problem if, like me, you are used to setting up vhost accounts in such a fashion:

/home/user/website.com/htdocs
/home/user/website.com/log
/home/user/website.com/cgi-bin

How can using flat files for mass virtual hosting be as easy to manage as databases one might ask? The answer is simple: generate the flat files from data stored in a database.

My configuration front-end sits on a web server that is independent from the one that serves the ‘public’ virtual hosts. This necessitated the ability to execute commands remotely as root, such as creating the directories for a new host, while at the same time taking into consideration the security implications of average users being able to pass data to the server as root.

My solution came in the form of a two-part system; a shared database that is used to pass sanity-checked data from the configuration interface and an administrative upkeep script run as root by cron every 5 minutes on the virtual hosting server. The script executes appropriate commands with the data set provided then flags its tasks as completed in the database. By storing arrays of raw data and indicating the job type one can avoid altogether the inherent problem of sending straight-up command line commands to the remote server. Careful variable checking in both the configuration interface where the data is added, then in the upkeep script where the tasks are then run can result in rock solid security, despite the fact that we are talking about translating a user’s actions on a web page to root-privileged commands.

In my frustration with the database modules for Apache I realized that the same system could be adapted to write, delete and overwrite individual apache configuration files based on back-end information. It helps to think of the situation like when one uses memcache or APC variable caching with an SQL database; the actual driving force behind the application is the database information but as it is pulled into memory the cache sits between them. By simply dropping the files into a directory and using a wildcard with an Include directive all it takes is reloading apache once updates have been performed for changes to take effect. Through maintaining a master database from which the files are generated one could easily delete the entire directory, run a regeneration script and they would all reappear.

It’s at this point one may find one’s head shaking: one of the prime benefits of database-backed configuration is that the apache server does not have to be restarted when a new vhost is added. Indeed, it is the very reason most people seem to switch to the solution. However I didn’t say restart, I said reload - it seems a lot of people have overlooked apache’s ability to reload configuration files gracefully – that is without being restarted all at once and without dropping open connections. Fortunately, I have not.

One of the major benefits of this approach weighed against database-only configuration is once apache has been reloaded all of the configuration is loaded into RAM; there is no need to worry about thread safety or hundreds of redundant connections and queries to your database server – a problem that becomes worse as your platform scales up without the aide of some sort of abstraction layer like pooled connections (mod_dbm support currently still in development (or not) for mod_vhs) or caching as could be implemented with much hackery and mysql-proxy.

This article will show you – in much simpler and specific detail – how I have implemented what I call an upkeep system that can manage virtual hosts and run privileged commands passed to it (from Joe User in his web browser) safely on a local or remote server with nothing more than PHP and MySQL. It is not at all hard to imagine this system being adapted to manage multiple servers, the workload distributed or logically divided among them given some automated mechanism or instruction from the configuration interface.

Bear in mind that it’s not the norm to use PHP for server-side-only scripts and you may wish to implement the idea in PERL or Python or something more traditional – but the application is sound, I’m good at PHP and I am secure in my masculinity :) PHP should be available on the web servers we intend to manage anyway, but may be more trouble than it’s worth to install it on, say, an NFS-only server if you want to split up file-related commands from configuration tasks.

The other drawback to this approach is the 5-minute delay between runs of the upkeep script. To address this I simply add a notification event to the database which the configuration interface searches for and reports to the user if they have any tasks pending completion.

First, create the shared table on a mutually-accessible SQL server. Remember to use one account for administration and one for the upkeep script, applying permissions frugally.

CREATE TABLE IF NOT EXISTS `upkeep` (
 `id` bigint(20) NOT NULL auto_increment,
 `uid` int(11) NOT NULL default '0',
 `date` int(11) NOT NULL default '0',
 `completed` int(11) NOT NULL default '0',
 `status` varchar(30) NOT NULL,
 `type` varchar(30) NOT NULL,
 `data` longtext NOT NULL,
 PRIMARY KEY  (`id`)
)

And we need one for the virtual hosts:

CREATE TABLE IF NOT EXISTS `virtual_hosts` (
 `id` int(11) NOT NULL auto_increment,
 `date` int(11) NOT NULL,
 `user` varchar(255) NOT NULL,
 `yuid` int(11) NOT NULL,
 `appid` int(11) NOT NULL,
 `server` char(255) NOT NULL,
 `environment_variable` char(255) default NULL,
 `subof` int(11) NOT NULL,
 `firewall` longtext NOT NULL,
 `errorpages` longtext NOT NULL,
 PRIMARY KEY  (`id`),
 KEY `yuid` (`yuid`),
 KEY `server` (`server`)
);

I’m using the uid field in the first table and the yuid field in the second to store the administrative interface’s account ID number or the person who uses the functions. Next we’re going to need some functions for the administration front-end to interface with:

function hostedUPKEEP($type, $data, $uid=0)
{
 if(is_array($data))
 $data = serialize($data);

 $data = mysql_real_escape_string($data);

 mysql_query("insert into `upkeep` (`uid`, `date`, `status`, `type`, `data`) values ('$uid', '".time()."', 'pending', '$type', '$data')");
}

function hostedUPKEEPMkdir($user, $group, $path)
{
 $data['user'] = $user;
 $data['group'] = $group;
 $data['path'] = $path;

 hostedUPKEEP('mkdir', $data);
}

function hostedVHOSTAddHost($user, $yuid, $appid, $server)
{
 $firewall = $errorpages = serialize(array());
 $date = time();

 mysql_query("insert into `virtual_hosts` (`user`, `date`, `yuid`, `appid`, `server`, `firewall`, `errorpages`) values ('$user', '$date', '$yuid', '$appid', '$server', '$firewall', '$errorpages')");
 $idgrabr = mysql_query("select * from ``virtual_hosts` where `user` = '$user' and `server` = '$server'");
 $idgrabo = mysql_fetch_object($idgrabr);

 hostedUPKEEPMkdir('root', 'root', "/home/$user");
 hostedUPKEEPMkdir($user, 'hosted', "/home/$user/$server");
 hostedUPKEEPMkdir($user, 'hosted', "/home/$user/$server/htdocs");
 hostedUPKEEPMkdir($user, 'hosted', "/home/$user/$server/log");
 hostedUPKEEPMkdir($user, 'hosted', "/home/$user/$server/cgi-bin");
 hostedUPKEEP('vhost', $idgrabo->id);
 hostedUPKEEP('notification', 'jobs pending notification', $yuid);
}

Since staff may edit user settings I pass the affected user’s front-end UID to a notification event, the user’s front-end will look for events marked pending with their UID and report that they must wait a little while for changes to take effect. Before passing any data to these functions it is important that you make sure it has been as carefully sanitized as possible. The following is a simple upkeep script that you can drop into /sbin/, chown root: and chmod 700 then add to cron at your preferred interval:

#!/usr/bin/php
<?php

$sql_host = '';
$sql_user = '';
$sql_pass = '';
$sql_base = '';

$sql_h = mysql_pconnect($sql_host, $sql_user, $sql_pass);
$sql_d = mysql_select_db($sql_base, $sql_h);

$forbidden_users = array('www',
'ftp',
'sql',
'mysql',
'database',
'db',
'sftp',
'ftps',
'sync',                                                                                                                          
'shutdown',                                                                                                                
'halt',                                                                                   
'mail',                                                                                                                 
'news',
'uucp',
'operator',
'calendar',
'docs',
'man',
'postmaster',
'cron',
'ftp',
'sshd',
'ssh',
'at',
'squid',
'gdm',
'xfs',
'games',
'named',
'postgres',
'apache',
'admin',
'administrator',
'cyrus',
'vpopmail',
'alias',
'qmaild',
'qmaill',
'qmailp',
'qmailq',
'qmailr',
'qmails',
'postfix',
'smmsp',
'portage',
'guest',
'nobody',
'clamav',
'amavis',
'vmail',
'ntp',
'deleted',
'mrtg',
'sockd',
'lighttpd',
'memcached',
'smokeping',
'rpc',
'anon',
'site',
'sites',
'anonymous',
'pop',
'pop3',
'smtp',
'sendmail',
'information_schema',
'test');

function checkname($string)
{
 global $forbidden_users;
 foreach($forbidden_users as $fuse)
 {
 if($fuse == strtolower($string))
 die("Forbidden User");
 }
}

$result = mysql_query("select * from `upkeep` where `status` = 'pending' order by `date` asc");
while($object = mysql_fetch_object($result))
{
 if($object->type == 'mkdir')
 {
 $data = unserialize($object->data);
 checkname($data['user']);
 checkname($data['group']);
 exec("mkdir -p ".escapeshellcmd($data['path']));
 exec("chown ".escapeshellcmd(ereg_replace("[^A-Za-z0-9]", '', $data['user'])).":".escapeshellcmd(ereg_replace("[^A-Za-z0-9]", '', $data['group']))." ".escapeshellcmd($data['path']));
 }

 if($object->type == 'vhost')
 {
 $id = $object->data;
 $vhost_result = mysql_query("select * from `virtual_hosts` where `id` = '$id'");
 $vhost_object = mysql_fetch_object($vhost_result);

 $domain = $vhost_object->server;
 $user = ereg_replace("[^A-Za-z0-9]", '', $vhost_object->user);

 checkname($user);

 $errarray = unserialize($vhost_object->errarray);
 if(!empty($errarray[0]))
 {
 $errorpages = '';
 foreach($errarray as $code => $loc)
 {
 $errorpages .= "\n\tErrorDocument $code $loc";
 }
 }
 else
 {
 $errorpages = '';
 }

 $fwarray = unserialize($vhost_object->fwrarray);
 if(!empty($fwarray[0]))
 {
 $firewall = '';
 foreach($fwarray as $address)
 {
 $firewall .= "\n\tDeny from $address";
 }
 }
 else
 {
 $firewall = '';
 }

 $domainesc = str_replace('.', '\.', $domain);

 $file = "<VirtualHost *:80>
\tServerName $domain
\tServerAlias www.$domain
\tDocumentRoot /home/$user/$domain/htdocs
\tScriptAlias /cgi-bin/ /home/$user/$domain/cgi-bin/
\tErrorLog /home/$user/$domain/log/error_log
\tTransferLog /home/$user/$domain/log/access_log{$errorpages}
\t<IfModule mod_rewrite.c>
\t\tRewriteEngine on
\t\tRewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$
\t\tRewriteRule .* - [F]
\t\tRewriteCond %{HTTP_HOST} ^www\.$domainesc$ [NC]
\t\tRewriteRule ^(.*)\$ http://$domainesc/\$1 [R=307,L]
\t</IfModule>
\t<IfModule mod_access.c>
\t\tOrder Allow,Deny
\t\tAllow from all{$firewall}
\t</IfModule>
</VirtualHost>";

 $fh = fopen("/etc/apache2/hosted.d/{$user}_{$domain}.conf", 'w');
 fwrite($fh, $file);
 fclose($fh);

 exec("/etc/init.d/apache2 reload");        // Change the path to your apache2ctl if the init script does not support reload.
 }

 mysql_query("update `upkeep` set `status` = 'completed', `completed` = '".time()."' where `id` = '{$object->id}'");
}

?>

Now create the directory /etc/apache2/hosted.d (or whatever you prefer) and add this directive to the end of your httpd.conf:

Include /etc/apache2/hosted.d/*.conf
Return top
foxpa.ws
Online Marketing Toplist
Internet
Technology Blogs - Blog Rankings

Internet Blogs - BlogCatalog Blog Directory

blogarama - the blog directory
Technology blogs
Bad Karma Networks

Please Donate!


Made in Canada  •  There's a fox in the Gibson!  •  2010-12