7
\$\begingroup\$

Here is a shell script which installs and configure owncloud on a Debian server. It also installs apache2 and MySQL as dependencies and fail2ban with a specific rule for owncloud. Apache2 is also configured to use a self signed SSL certificate.

The script seems to work properly (everything is installed and works fine) my question is about the possible security issues that could be created:

  • Is the ssl certificate correctly issued and installed?
  • Is there a potential issue with the handling of mysql?
  • Is there something else that I should see?

The official Github repo of this file is here.

#!/bin/bash

#Some bash script optimization for robustness (More information: www.davidpashley.com/articles/writing-robust-shell-scripts/)
#Break if the script uses unset variables
set -o nounset
#Break if a command has a non-true return value
set -o errexit

echo_info () {
    echo "--------------------------------------"
    echo "| INFO: $1"
    echo "--------------------------------------"
}

echo_error () {
    echo "--------------------------------------"
    echo "| ERROR: $1"
    echo "--------------------------------------"
}

is_installed () {
    #Check if a package is installed (More information: https://askubuntu.com/questions/319307/reliably-check-if-a-package-is-installed-or-not)
    if dpkg --get-selections | grep -q "^$1[[:space:]]*install$" >/dev/null; then
        echo_error "$1 is already installed"
        exit 1
    fi
}

check_not_installed () {

    echo_info "Check if some package is already installed. If this is the case the script stops because it could not be performed without risc."

    is_installed apache2
    is_installed mysql-server-5.5
    is_installed owncloud
    is_installed fail2ban 

}

check_root () {
    if [ "$(id -u)" != "0" ]; then
        echo_error "The script must be called as root user!"
        exit 1
    fi
}

install_owncloud () {

    cd /tmp

    #Automatic installation of owncloud (More information: https://software.opensuse.org/download/package?project=isv:ownCloud:community&package=owncloud)

    echo_info "Add package repository"

    echo 'deb http://download.owncloud.org/download/repositories/8.2/Debian_8.0/ /' >> /etc/apt/sources.list.d/owncloud.list

    wget -nv https://download.owncloud.org/download/repositories/8.2/Debian_8.0/Release.key -O Release.key
    apt-key add - < Release.key  
    rm Release.key  

    echo_info "Install owncloud"

    #Because a dependency the package mysql-server-5.5 is installed too. During the installation a prompt ask for the root password. The next lines set it with the value of the variable. (More information: http://www.microhowto.info/howto/perform_an_unattended_installation_of_a_debian_package.html)
    echo "mysql-server-5.5 mysql-server/root_password password $mysqlRootPw" | debconf-set-selections
    echo "mysql-server-5.5 mysql-server/root_password_again password $mysqlRootPw" | debconf-set-selections

    apt-get update
    apt-get --assume-yes install owncloud

}

create_mysql_db () {

    echo_info "Create mysql database for owncloud"

    #Create a new database and user for owncloud (More information: http://www.bluepiccadilly.com/2011/12/creating-mysql-database-and-user-command-line-and-bash-script-automate-process)
    mysql=`which mysql`

    Q1="CREATE DATABASE IF NOT EXISTS $ocDb;"
    Q2="GRANT USAGE ON *.* TO $ocDbUser@localhost IDENTIFIED BY '$ocDbUserPw';"
    Q3="GRANT ALL PRIVILEGES ON $ocDb.* TO $ocDbUser@localhost;"
    Q4="FLUSH PRIVILEGES;"
    SQL="${Q1}${Q2}${Q3}${Q4}"

    $mysql -uroot -p$mysqlRootPw -e "$SQL"

}

patch_apache_filesize () {

    echo_info "Patch apache configuration"

    #Patch filesize in the htaccess (More Information: http://blog.webernetz.net/2015/07/15/yet-another-owncloud-installation-guide/)
    sed -i "s/php_value upload_max_filesize .*/php_value upload_max_filesize $maxFileSize/" /var/www/owncloud/.htaccess
    sed -i "s/php_value post_max_size .*/php_value post_max_size $maxFileSize/" /var/www/owncloud/.htaccess
    sed -i "s/php_value memory_limit .*/php_value memory_limit $maxFileSize/" /var/www/owncloud/.htaccess

    /etc/init.d/apache2 restart
}

configure_owncloud () {

    echo_info "Configure owncloud"

    #The data dir should not be under /var/www for security reasons, so the dir must be created (More information: https://doc.owncloud.org/server/8.1/admin_manual/configuration_server/harden_server.html)
    mkdir $ocDataDir
    chown -R ${htuser}:${htgroup} ${ocDataDir}/

    #Use the cli for first configuration (More information: https://doc.owncloud.org/server/8.1/admin_manual/installation/command_line_installation.html)
    cd /var/www/owncloud
    sudo -u $htuser php occ maintenance:install --database "mysql" --database-name "$ocDb"  --database-user "$ocDbUser" --database-pass "$ocDbUserPw" --admin-user "$ocAdminUser" --admin-pass "$ocAdminUserPw" --data-dir "$ocDataDir"

        #Add the hostname and ip to the trusted domains, so that it could be reached from outside (More Information: https://doc.owncloud.org/server/8.1/admin_manual/installation/installation_wizard.html?highlight=trusted_domains#label-trusted-domains)
    sed -i "/.*0 => 'localhost',/a \\    1 => '$hostname',\n    2 => '$ip'," /var/www/owncloud/config/config.php

    /etc/init.d/apache2 restart

}

install_fail2ban () {

    echo_info "Install fail2ban"

    apt-get --assume-yes install fail2ban

    #First configure the owncloud logfile
    logFileMasked=$(echo $logFile | sed 's/\//\\\//g')
    logTimezoneMasked=$(echo $logTimeZone | sed 's/\//\\\//g')
    sed -i "s/  'logtimezone' => 'UTC',/  'logtimezone' => '$logTimezoneMasked',\n  'logfile' => '$logFileMasked',\n  'loglevel' => '2',/" /var/www/owncloud/config/config.php

    touch $logFile
    chown ${htuser}:${htgroup} $logFile

    #Now configure fail2ban (More Information: http://www.rojtberg.net/711/secure-owncloud-server/, https://got-tty.org/archives/owncloud-6-sicherheit-durch-fail2ban.html)
    echo -e "[Definition]\nfailregex={\"app\":\"core\",\"message\":\"Login failed: user '.*' , wrong password, IP:<HOST>\",\"level\":2,\"time\":\".*\"}\n          {\"app\":\"core\",\"message\":\"Login failed: '.*' \(Remote IP: '<HOST>', X-Forwarded-For: '.*'\)\",\"level\":2,\"time\":\".*\"}\n          {\"reqId\":\".*\",\"remoteAddr\":\"<HOST>\",\"app\":\"core\",\"message\":\"Login failed: .*\",\"level\":2,\"time\":\".*\"}" > /etc/fail2ban/filter.d/owncloud.conf

    echo -e "[owncloud]\nenabled  = true\nfilter   = owncloud\nport     = http,https\nmaxretry = $maxRetry\nlogpath  = $logFile" >> /etc/fail2ban/jail.local

    /etc/init.d/fail2ban restart
}

enable_apache_ssl () {

    echo_info "Enable and compel apache ssl with default self-signed certifiacte of debian"

    #Uses the default self-signed certificate of debian (More information: https://doc.owncloud.org/server/8.1/admin_manual/installation/source_installation.html#enabling-ssl)
    a2enmod ssl
    a2ensite default-ssl

    #Force https for every connection (More information: https://doc.owncloud.org/server/8.1/admin_manual/configuration_server/harden_server.html)
    a2enmod headers
    sed -i "/.*<VirtualHost.*/a \\\tServerName $hostname\n\tRedirect permanent \/ https:\/\/$hostname\/" /etc/apache2/sites-available/000-default.conf
    sed -i "/.*<VirtualHost.*/a \\\t\tServerName $hostname\n\t\tHeader always add Strict-Transport-Security \"max-age=15768000\"" /etc/apache2/sites-available/default-ssl.conf

    /etc/init.d/apache2 restart
}

enable_apc_cache () {

    echo_info "Enable apc cache"
    #Install and configure apcu (More information: https://owncloud.org/blog/making-owncloud-faster-through-caching/)
    apt-get --assume-yes install php-apc
    sed -i "s/);/  'memcache.local' => '\\\OC\\\Memcache\\\APCu',\n);/" /var/www/owncloud/config/config.php

    /etc/init.d/apache2 restart
}

#Read in the variables in an interacive mode. Too make it a little more comfortable, the following functions need to be defined.

#Read a value and set the default value as input (More Information: http://stackoverflow.com/questions/2642585/read-a-variable-in-bash-with-a-default-value)
read_value () {
    unset value
    read -e -i $1 value
}

#Hide the input and mask it with stars (More Information: http://stackoverflow.com/questions/1923435/how-do-i-echo-stars-when-reading-password-with-read) (Part of the read_pw function)
read_pw_loop_masked () {
    unset password
    while IFS= read -p "$prompt" -r -s -n 1 char
    do
        if [[ $char == $'\0' ]]
        then
            break
        fi
        prompt='*'
        password+="$char"
    done
    echo
}

#Ask for the password two times (Part of the read_pw function)
read_pw_loop_compare () {
    prompt="Enter Password:"
    read_pw_loop_masked
    password1=$password
    prompt="Reenter Password:"
    read_pw_loop_masked
    password2=$password
}

#Loop the password question until the two values match (Part of the read_pw function)
read_pw_loop () {
    read_pw_loop_compare
    while [ "$password1" != "$password2" ] ; do
        echo "Please retype, because the passwords did not match."
        read_pw_loop_compare    
    done
    password=$password1
}

#Main function for reading a password
read_pw () {
    echo $*
    read_pw_loop

}

ask_for_values () {
    echo_info "The script now ask for some values that are necessary for the installation."
    #You can customize the default values of the variables here or set a static value

    #Hostname and IP (the command retrieve this information automatically in the case that only the standard ethernet interface is installed)
    hostname=`hostname`
    echo "Please enter the hostname (The automatically indentified value is filled in, but you can easily change it if it is wrong.):"
    read_value $hostname
    hostname=$value
    ip=`hostname -I`
    echo "Please enter the ip of the outside interface (The automatically indentified value is filled in, but you can easily change it if it is wrong.):"
    read_value $ip
    ip=$value

    #Mysql configuration
    read_pw "Please enter a password for the root user of mysql:"
    mysqlRootPw=$password

    echo "Please enter the name of the mysql database for owncloud (The default value is filled in, but you can easily change it.):"
    read_value owncloud
    ocDb=$value

    echo "Please enter the name of the mysql user for owncloud (The default value is filled in, but you can easily change it.):"
    read_value owncloud
    ocDbUser=$value

    read_pw "Please enter a password for the owncloud user of mysql:"
    ocDbUserPw=$password

    #Apache2 configuration
    echo "Please enter the maximal size of files that could be uploaded to owncloud (The default value is filled in, but you can easily change it.):"
    read_value 1024M
    maxFileSize=$value
    #Typically user and group filled static
    htuser='www-data'
    htgroup='www-data'

    #Owncloud configuration

    echo "Please enter the name of the owncloud administrator(The default value is filled in, but you can easily change it.):"
    read_value admin
    ocAdminUser=$value

    read_pw "Please enter a password for the owncloud administrator:"
    ocAdminUserPw=$password

    echo "Please enter the path to the folder for files of owncloud (The default value is filled in, but you can easily change it.):"
    read_value /home/owncloud
    ocDataDir=$value

    #Fail2Ban
    logTimeZone=`cat /etc/timezone`
    echo "Please enter the time zone for the owncloud log (The default value is filled in, but you can easily change it.):"
    read_value $logTimeZone
    logTimeZone=$value

    echo "Please enter the path where owncloud log should be saved (The default value is filled in, but you can easily change it.):"
    read_value /var/log/owncloud.log
    logFile=$value

    echo "Please enter the max fails until fail2ban ban an ip (The default value is filled in, but you can easily change it.):"
    read_value 3
    maxRetry=$value

}

generate_self_signed_certificate () {

    echo_info "The script now generate a self signed certificate with a self created ca. For it the script ask for many values. The most necessary you can fill with a dot if you do not have a valid value. The 'extra' attributes you could skip with enter. Only the pass phrase for own-ca.key and the common name are important. The first value is the password for your ca, please type in a strong password and take a note of it. You will need it at least some seconds later. The common name must befit the hostname and you must type it in two times."

    #Generate a self signed certificate with a self created ca (More Information: https://thomas-leister.de/internet/eine-eigene-openssl-ca-erstellen-und-self-signed-certe-ausstellen/)
    # This was necessary because otherwise Apps like DAVdroid didn't work with the owncloud (More Information: https://davdroid.bitfire.at/faq/entry/importing-a-certificate)

    #Create a new ca
    mkdir /etc/ssl/ownca/
    cd /etc/ssl/ownca/
    openssl genrsa -aes256 -out own-ca.key 2048
    #Create the root certificate that is valid for 10 years
    openssl req -x509 -new -nodes -extensions v3_ca -key own-ca.key -days 3650 -out own-ca-root.pem -sha512
    #Create a client certificate that is valid for 10 years and sign it
    openssl genrsa -out self-signed-cert.key 4096
    openssl req -new -key self-signed-cert.key -out self-signed-cert.csr -sha512
    openssl x509 -req -in self-signed-cert.csr -CA own-ca-root.pem -CAkey own-ca.key -CAcreateserial -out self-signed-cert.pem -days 3650 -sha512
    rm self-signed-cert.csr
    mv ./self-signed-cert.pem ../certs
    mv ./self-signed-cert.key ../private
    mv ./own-ca-root.pem ../certs

}

install_self_signed_certificate () {
    sed -i "s/      SSLCertificateFile  \/etc\/ssl\/certs\/ssl-cert-snakeoil.pem/       SSLCertificateFile  \/etc\/ssl\/certs\/self-signed-cert.pem/" /etc/apache2/sites-available/default-ssl.conf
    sed -i "s/      SSLCertificateKeyFile \/etc\/ssl\/private\/ssl-cert-snakeoil.key/       SSLCertificateKeyFile \/etc\/ssl\/private\/self-signed-cert.key/" /etc/apache2/sites-available/default-ssl.conf
    sed -i "s/      #SSLCertificateChainFile \/etc\/apache2\/ssl.crt\/server-ca.crt/        SSLCertificateChainFile \/etc\/ssl\/certs\/own-ca-root.pem/" /etc/apache2/sites-available/default-ssl.conf
    /etc/init.d/apache2 restart
}

#You can customize the called functions here (you are responsible for looking for dependencies between them)
check_root
check_not_installed
ask_for_values
generate_self_signed_certificate
echo_info "Now the unattended part of the setup is started."
install_owncloud
create_mysql_db
patch_apache_filesize
configure_owncloud
install_fail2ban
enable_apache_ssl
enable_apc_cache
install_self_signed_certificate

echo_info "FINISH"
\$\endgroup\$
2
\$\begingroup\$

Naming

Some of the function names could be improved, for example this one:

is_installed () {
    #Check if a package is installed (More information: https://askubuntu.com/questions/319307/reliably-check-if-a-package-is-installed-or-not)
    if dpkg --get-selections | grep -q "^$1[[:space:]]*install$" >/dev/null; then
        echo_error "$1 is already installed"
        exit 1
    fi
}

I expect a function named is_X to behave like a boolean, so that I can build conditional statements with it. But this is not that kind of function, it validates something or else exits the program. I'd name this verify_not_installed or similar.

Other minor issues

if dpkg --get-selections | grep -q "^$1[[:space:]]*install$" >/dev/null; then

You don't need to redirect the output of grep -q, the -q flag already takes care of that.


echo_error "$1 is already installed"
exit 1

Every time you echo_error, you also exit 1. So you could move the exit 1 inside the function, and then probably rename it (I usually call it fatal).


echo 'deb http://download.owncloud.org/download/repositories/8.2/Debian_8.0/ /' >> /etc/apt/sources.list.d/owncloud.list

wget -nv https://download.owncloud.org/download/repositories/8.2/Debian_8.0/Release.key -O Release.key

The base URL is common in both of these commands, so I would put it in a variable to make it easy to change later if needed.


mysql=`which mysql`

Backticks are archaic, use $(...) instead.


Q1="CREATE DATABASE IF NOT EXISTS $ocDb;"
Q2="GRANT USAGE ON *.* TO $ocDbUser@localhost IDENTIFIED BY '$ocDbUserPw';"
Q3="GRANT ALL PRIVILEGES ON $ocDb.* TO $ocDbUser@localhost;"
Q4="FLUSH PRIVILEGES;"
SQL="${Q1}${Q2}${Q3}${Q4}"

This is a tedious and fragile way to construct a longer string. You could pass a here-document directly to mysql:

mysql -uroot -p$mysqlRootPw <<EOF
CREATE DATABASE IF NOT EXISTS $ocDb;
GRANT USAGE ON *.* TO $ocDbUser@localhost IDENTIFIED BY '$ocDbUserPw';
...
EOF

sed -i "s/php_value upload_max_filesize .*/php_value upload_max_filesize $maxFileSize/" /var/www/owncloud/.htaccess
sed -i "s/php_value post_max_size .*/php_value post_max_size $maxFileSize/" /var/www/owncloud/.htaccess
sed -i "s/php_value memory_limit .*/php_value memory_limit $maxFileSize/" /var/www/owncloud/.htaccess

This overwrites the /var/www/owncloud/.htaccess three times. You can perform multiple replacements in a single sed call, and overwrite the file only once:

sed -i \
  -e "s/php_value upload_max_filesize .*/php_value upload_max_filesize $maxFileSize/" \
  -e "s/php_value post_max_size .*/php_value post_max_size $maxFileSize/" \
  -e "s/php_value memory_limit .*/php_value memory_limit $maxFileSize/" \
  /var/www/owncloud/.htaccess

echo -e "[Definition]\nfailregex={\"app\":\"core\",\"message\":\"Login failed: user '.*' , wrong password, IP:<HOST>\",\"level\":2,\"time\":\".*\"}\n          {\"app\":\"core\",\"message\":\"Login failed: '.*' \(Remote IP: '<HOST>', X-Forwarded-For: '.*'\)\",\"level\":2,\"time\":\".*\"}\n          {\"reqId\":\".*\",\"remoteAddr\":\"<HOST>\",\"app\":\"core\",\"message\":\"Login failed: .*\",\"level\":2,\"time\":\".*\"}" > /etc/fail2ban/filter.d/owncloud.conf

This extremely long line is unreadable. Consider using a here-document instead:

cat <<"EOF" > /etc/fail2ban/filter.d/owncloud.conf
[Definition]
failregex={"app":"core","message":"Login failed: user '.*' , wrong password, IP:<HOST>","level":2,"time":".*"}
          {"app":"core","message":"Login failed: '.*' \(Remote IP: '<HOST>', X-Forwarded-For: '.*'\)","level":2,"time":".*"}
          {"reqId":".*","remoteAddr":"<HOST>","app":"core","message":"Login failed: .*","level":2,"time":".*"}
EOF
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.