Posted on May 31, 2010 by Mikko Ohtamaa Filed Under apache, linux, technology, ubuntuTags: apache, email. chmod, install, installation, joomla, linux, mysql, permissions, shell, sudo, ubuntu, unix, virtualhost
This how to shorty explains how to set-up a Joomla! hosting on a shared hosting server you own to have basic security. This instructions apply for Debian/Ubuntu based systems, but can be generalized to any Linux based system like Fedora.
In this how to we use the following software versions
- Joomla 1.5
- Apache 2.2
- MySQL 5.1
- Ubuntu 8.04 Hardy Heron server edition
The instructions may apply for other versions too.
Prerequisitements
What you need to have in order to use this how to
- Basic UNIX file permissions knowledge
- Basic UNIX shell knowledge
- You have a Linux server (Ubuntu / Debian) for which you have root user access and you plan to use this server to host one or several Joomla! sites
- Apache and MySQL instaleld on your server
User setup
Set-up an UNIX user on a dedicated server for Joomla! hosting. The user can SSH in the box and write to his home folder, /tmp and /var/www site folder.
We create a user called “user” in this instructions. Replace it with the username you desire. We also use the example site name (www).yoursite.com.
Create new UNIX user and /home/user folder.
sudo adduser user # Asks for the password and created /home/user
Create corresponding /var/www/user folder.
sudo mkdir /var/www/user
sudo chmod -R user:user /var/www/user # Only user has writing access to this folder
Setup MySQL user account
Install MySQL as per Debian/Ubuntu instructions.
Login as MySQL admin user (may vary depending how your MySQL is configured). Note that first you will be asked for sudo password, then for MySQL administrative user password.
sudo mysql -u admin -p
Then create a new database with the same name as new as the UNIX user. Make sure that we use UTF-8 character encoding so we avoid irritating encoding problems in the future.
CREATE DATABASE user DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
Create a MySQL user with the same name as the UNIX user. Use a random password and give it all rights for the database. Note that this password should differ from the UNIX username password as this must be stored as plain-text in Joomla PHP files. Also MySQL differs users whether they came from localhost or other IP address. Here we use localhost so that the database is connectable only from the same server as Apache is running.
GRANT ALL ON user.* TO 'user'@'localhost' identified by 'zxc123zxc';
Extract Joomla! installation files
Enter the folder which will contain web site PHP files.
sudo -i -u user # pose yourself as UNIX user who runs the site
cd /var/www/user
wget http://joomlacode.org/gf/download/frsrelease/12350/51111/Joomla_1.5.18-Stable-Full_Package.zip
Unzip it.
unzip Joomla_1.5.18-Stable-Full_Package.zip
Exit posing yourself as user UNIX user.
exit
Set file permission
In order to secure your server
- Configuration files and upload directory must be writable by Apache user (www-data for Ubuntu/Debian, httpd for Fedora/Red Hat)
- Other .php files should be read-only
Note that during Joomla’s browser based installation Apache’s www-data must have write access to folder in order to create configuration.php file. We will later remove this access right.
We will set Joomla! files under UNIX group group www-data so that Apache can read them. Certain files are set to be writable. This must be done as root user.
sudo chown -R user:www-data /var/www/user # Make user group to www-data
sudo chmod g+wrx /var/www/user # Read only access to www-data user. Write access for installation, will be later removed.
Now ls -l command in /var/www/user should give you something like this for fil masks:
drwxr-xr-x 11 user www-data 4096 2010-05-28 10:22 plugins
-rwxr--r-- 1 user www-data 304 2010-05-28 10:21 robots.txt
drwxr-xr-x 6 user www-data 4096 2010-05-28 10:22 templates
Creating Apache configuration
This allows serving Joomla! by Apache and starting the browser based configuration.
First create Apache configuration file under /etc/apache2/sites-enabled as root user. We assume nano terminal base text editor is installed on the server.
sudo nano /etc/apache2/sites-enabled/yoursite.conf
Below is a sample configuration file. You may need to match your server public IP in <virtualhost, so that Apache knows for which IP address sites are served. We use virtual hosting: every site on the server is identified by incoming HTTP request.
<VirtualHost *>
ServerName yoursite.com
ServerAlias www.yoursite.com
ServerAdmin info@yourcompany.com
LogFormat combined
TransferLog /var/log/apache2/yoursite.log
# Make sure this virtual host if capable of executing PHP5
Options +ExecCGI
AddType application/x-httpd-php .php .php5
# Point to www folder where Joomla! is extracted
DocumentRoot /var/www/yoursite
# Do not give illusion of safety
# as PHP safe_mode really is a crap
# and only causes problems
php_admin_flag safe_mode off
#
# This entry will redirect traffic www.yoursite.com -> yoursite.com
# Assume mod_rewrite is installed and enabled on Apache
# 301 is HTTP Permanent Redirect code
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.yoursite\.com [NC]
RewriteRule (.*) http://yoursite.com$1 [L,R=301]
</VirtualHost>
Faking the DNS entry
If you have not yet reserved a domain name for your site, but still want to get the virtual host working, you can add a DNS name entry into a
hosts file on your local computer. The following assumes you are using Ubuntu desktop, but
hosts file is available on Windows and OSX too.
sudo gedit /etc/hosts
Then add the lines like the example below. Do not forget to remove this from hosts file when the actual DNS has been set up.
# Force this hostname to go to your server public IP address from your local computer
123.123.123 yoursite.com www.yoursite.com
Start Joomla! browser based installation
Then enter the URL of your site to the browser:
http://yoursite.com
Joomla! installation page should appear.
- Fill in MySQL database values as created before.
- If you plan to use SSH for file transfer do not enable FTP layer (unsecure).
- Use a random password as Joomla! administrator user and store it somewhere in safe.
- When Joomla! browser based installation goes to the point it asks you to remove the installation directory follow the instructions below.
Secure the configuration
Now remove extra permissions from Apache’s www-data user so that in the case there is a PHP / Joomla security hole, your site files cannot get compromised.
Some folders must remain writable as Joomla! will upload or write files in them.
sudo chmod -R g-w /var/www/user # Remote write permission
sudo rm -rf /var/www/user/installation # Remove installation directory
# Add write permission to folders which contain writable files
sudo chmod -R g+x /var/www/user/logs
sudo chmod -R g+x /var/www/user/images
sudo chmod -R g+x /var/www/user/tmp
sudo chmod -R g+x /var/www/user/images
Setting up htaccess files
Joomla! comes with a sample htaccess file which has some security measurements by having RewriteRules to prevent malformed URL access.
To install this file do the following
sudo -i
cd /var/www/user
cp htaccess.txt .htaccess
chmod user:www-data .htaccess # Set file permission to be readable by Apache and writable by the UNIX user
Then we create a .htaccess file which we will place in all folders with Joomla! write access to prevent execution of PHP files in these folders. First we create htaccess.limited file which we use as a template.
sudo -i
cd /var/www/user
nano htaccess.limited # Open text editor
Use the following htaccess.limited content
# secure directory by disabling script execution
AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi
Options -ExecCGI -Indexes
And put the master template htaccess.limited to proper places
cp htaccess.limited media/.htaccess
chown -R user:www-data media/.htaccess
cp htaccess.limited tmp/.htaccess
chown -R user:www-data tmp/.htaccess
cp htaccess.limited logs/.htaccess
chown -R user:www-data logs/.htaccess
cp htaccess.limited images/.htaccess
chown -R user:www-data images/.htaccess
Start using the site
Now go to your site with the browser again and Joomla! start page should come up.
Login as administration account you gave in Joomla! browser based installation.
Type URL http://yoursite.com in your browser.
Setting outgoing email
This is probably first thing you want to do as Joomla! administrator. You configure the SMTP server which will be used for outgoing email. The server is usually provided by network operator who provides the internet connection for your server.
Login as Joomla! administrator user.
Go to Site -> Global Configuration -> Server.
Choose SMTP mail mode.
Enter SMTP details.
Test outgoing email
Create a new user with an email address you control The user should receive New User Details email message from the site on the moment the user is created.
Maintaining file permission
If you modify or create any files (e.g. upload a new theme) to your server you need to set file permissions for it.
- UNIX user: user (your site username)
- UNIX group: www-data
To make it possible to set the group ownership with user user you first need to add it to www-data group.
sudo usermod -a -G www-data user # Add user to www-data group so that it can set group permissions
Then you can fix the permissions for uploaded files (templates and libraries folders assumed)
sudo -i -u user # Login as your UNIX user
chgrp -R www-data templates libraries # Fix group ownership
chmod -R g+rx libraries templates # Set read access for the group
This way secure file permissions are fixed after files have been changed. Alternatively, if your secure SFTP program supports setting permissions during the file upload, you can use that option
Read our blog
Subscribe mFabrik blog in a reader
Follow us on Twitter
Mikko Ohtamaa on LinkedIn
Posted on October 3, 2008 by Mikko Ohtamaa Filed Under Plone (old), pythonTags: access, assert, borg, compile_restricted, eval, guard, local roles, localroles, permissions, Plone (old), python, restricted, sandbox, security, securitymanagement, unauthorized, unit test, unit testing, zope
Security is hard. Unit testing security in Plone seems to be even harder. Here is a fool proof example how to do it. After comments I plan to release this as plone.org How to. I hope some of these ideas could get into PloneTestCase itself, so there wouldn’t be need to reinvent the wheel on every product.
Since 2004, when I was first introduced to Plone, it has been great mystery to me how to properly unit test your content type and workflow security declarations. Archetypes itself uses ugly hack where it creates secure Python Scripts from strings in Zope and then executes them. There had to be something better, but after asking questions no one seem to know what.
Function security declarations (security.declareProtected & co.) are only effective when Python is run in restricted mode. Entering to “restricted Zope Python” has not been very well documented anywhere, until RestrictedPython package Read me got revamped. This finally gave a clue how to one could hit Unauthorized exceptions in unit testing.
To enter the promised world of sandboxed Python you need to do following
- Create a globals dictionary containing secured version of all __builtin__ functions and accessable objects
- Compile your Python code through RestrictedPython compiler
- Evaluate the result
Zope get_safe_globals() will overwrite __getattr__ with guarded_getattr, etc. providing automatic code execution level security. This information is not usable only for unit testing, but for scripting purposes also – it is a developer heaven to be able to give a sandboxed template environment to the users to play around withoutworry that they can escalate privileges.
But getting into restricted mode was not enough… after that all kind of kinks started to hit me. Namely, in some places of Plone items are cached over the request lifecycle. Since unit tests do not create new requests, the cache will contain invalid values. Here borg.localroles bit me badly – I had to dig through the security management layers manually to see why the unit test code was giving bad results. Maybe it would be wise to have a flag for caches and disable them when running on a test layer?
Below is the my example code for normal Document content type and simple_publication_workflow. All sandboxed code are declared in independend functions, but it is easy to pass arguments for them. If there is no need to reuse the sandboxed functions, I recommend use Python lambda: function declaration.
Functions which should succesfully pass sandbox testing are evaluated using self.execUntrusted(). Functions which are expect to fail are evaluated using self.assertUnauthorized().
import unittest
# Zope security imports
from AccessControl import getSecurityManager
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import noSecurityManager
from AccessControl.SecurityManager import setSecurityPolicy
from AccessControl import ZopeGuards
from AccessControl.ZopeGuards import guarded_getattr, get_safe_globals
from AccessControl.ImplPython import ZopeSecurityPolicy
from AccessControl import Unauthorized
# Restricted Python imports
from RestrictedPython import compile_restricted
from RestrictedPython.Guards import safe_builtins
from RestrictedPython.SafeMapping import SafeMapping
from zope.component import getUtility, getMultiAdapter, getSiteManager
from Products.CMFCore.tests.base.security import UserWithRoles
from Products.CMFCore.WorkflowCore import WorkflowException
from Products.CMFCore.utils import getToolByName
__docformat__ = "epytext"
__author__ = "Mikko Ohtamaa <mikko@redinnovation.com>"
__license__ = "BSD"
class WorkflowTestCase(PloneTestCase):
""" Test workflow access rights. """
def afterSetUp(self):
self.workflow = getToolByName(self.portal, 'portal_workflow')
self.acl_users = getToolByName(self.portal, 'acl_users')
self.types = getToolByName(self.portal, 'portal_types')
self.registration = getToolByName(self.portal, 'portal_registration')
self.membership = getToolByName(self.portal, 'portal_membership')
# Create a normal registered portal member
# to be used in tests
self.registration.addMember("testmember", "secret", ["Member",], properties={ 'username': "testmember", 'email' : "foobar@foobar.com" })
# Set verbose security policy, making debugging Unauthorized
# exceptions great deal easier in unit tests
setSecurityPolicy(ZopeSecurityPolicy(verbose=True))
def clearLocalRolesCache(self):
""" Clear borg.localroles cache.
borg.localroles check role implementation caches user/request combinations.
If we edit the roles for a user we need to clear this cache,
"""
from zope.annotation.interfaces import IAnnotations
ann = IAnnotations(self.app.REQUEST)
for key in ann.keys():
del ann[key]
def loginAsPortalMember(self, id):
''' Login as a normal portal member.
@param id. username
'''
self.login(id)
def _execUntrusted(self, debug, func, *args, **kwargs):
""" Sets up a sandboxed Python environment with Zope security in place.
Calls func() in an sandboxed environment. The security mechanism
should catch all unauthorized function calls (declared
with a class SecurityManager).
Security is effective only inside the function itself -
The function security declarations themselves are ignored.
@param func: Function object
@param args: Parameters delivered to func
@param kwargs: Parameters delivered to func
@param debug: If True, break into pdb debugger just before evaluation
@return: Function return value
"""
# Create global variable environment for the sandbox
globals = get_safe_globals()
globals['__builtins__'] = safe_builtins
globals['_getattr_'] = guarded_getattr
# Create variable context available in the restricted Python
data = { "func" : func,
"args" : ZopeGuards.SafeIter(args),
"kwargs" : kwargs } # TODO: Do we need to map this to SafeMappings?
globals.update(data)
# Our magic code
body = """func(*args, **kwargs)"""
# The following will replace all function calls
# in the code with Zope call guard proxies
code = compile_restricted(body, "<string>", "eval")
# Here is a good place to break in
# if you need to do some ugly permission debugging
if debug:
import pdb
pdb.set_trace()
return eval(code, globals)
def execUntrusted(self, func, *args, **kwargs):
""" Sets up a sandboxed Python environment with Zope security in place. """
return self._execUntrusted(False, func, *args, **kwargs)
def execUntrustedDebug(self, func, *args, **kwargs):
""" Sets up a sandboxed Python debug environment with Zope security in place. """
return self._execUntrusted(True, func, *args, **kwargs)
def assertUnauthorized(self, func, *args, **kwargs):
""" Check that calling func with currently effective roles will raise Unauthroized error. """
try:
self.execUntrusted(func, *args, **kwargs)
except Unauthorized, e:
return
raise AssertionError, 'Unauthorized exception was expected'
def test_document_workflow_access(self):
""" Check that anonymous users cannot access diagnosis in unwanted state. """
def check_set_access(doc, text="foobar"):
""" This is executed as RestrictedPython, print might not be available """
# Try do a call which should hit Zope and Archetypes field security mechanisms
doc.setText(text)
def check_read_access(doc):
""" This is executed as RestrictedPython, print might not be available """
# Try do a call which should hit Zope and Archetypes field security mechanisms
return doc.getText()
def check_workflow_action(portal, action):
""" Publish the document.
Stresses secure workflow execution
"""
portal.portal_workflow.doActionFor(portal.doc, action)
# Login as a manager and create
# an item which is initially private page to play around with
self.loginAsPortalOwner()
self.portal.invokeFactory("Document", "doc")
doc = self.portal.doc
# Item is private by default and editably by creator
self.execUntrusted(check_set_access, doc)
self.logout()
# Anonymous cannot access the document when it's private
self.assertUnauthorized(check_read_access, doc)
self.assertUnauthorized(check_set_access, doc)
# Relogin as a normal member and see we cannot access the item
self.loginAsPortalMember("testmember")
self.assertUnauthorized(check_set_access, doc)
self.logout()
# Now relogin as the manager and share manager role with a member
self.loginAsPortalOwner()
self.membership.setLocalRoles(obj=doc,
member_ids=["testmember"],
member_role="Owner",
reindex=True)
# IMPORTANT: This is a very invisible feature of Plone 3.1 -
# setLocalRoles is ineffective in unit tests unless the cache is cleared
self.clearLocalRolesCache()
self.logout()
# Relogin as a normal member and now we should be able to edit the document
self.loginAsPortalMember("testmember")
doc = self.portal.doc
# Rich text is automatically paragraphed unless it
# begins with HTML element
self.assertEqual(self.execUntrusted(check_read_access, doc), "<p>foobar</p>")
self.execUntrusted(check_set_access, doc)
self.execUntrusted(check_workflow_action, self.portal, "submit")
# Only site manager can publish items
try:
self.execUntrusted(check_workflow_action, self.portal, "publish")
raise AssertionError("Publishing as normal member should not be possible")
except WorkflowException:
# WorkflowException: No workflow provides the '${action_id}' action.
pass
self.logout()
# Now the portal owner publishes the document
self.loginAsPortalOwner()
self.execUntrusted(check_workflow_action, self.portal, "publish")
self.logout()
# Anonymous should now have read access/no edit
self.execUntrusted(check_set_access, doc)
self.assertEqual(self.execUntrusted(check_read_access, doc), "<p>foobar</p>")
# Member should be still able to read and edit the document
self.loginAsPortalMember("testmember")
self.assertEqual(self.execUntrusted(check_read_access, doc), "<p>foobar</p>")
self.logout()
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(WorkflowTestCase))
return suite