在 Ubuntu18.04 安装 LEMP 栈,构建本地网站

0x00 背景

1. 目的

基于 LEMP 栈 搭建本地 web 服务器,实现一个能在本地网络中访问的网页,网页中实现对数据库的简单操作。

学习目标:

  • 理解 web 服务器的运行,学会手动搭建一个 web 服务器
  • 学习简单的 HTML 和 PHP 知识,并编写网站前端和服务器端的代码
  • 学习 MySQL 数据库的基本知识,完成数据库的基本操作,实现 PHP 和 MySQL 的连接
  • 学习如何加固 LEMP 栈

2. LEMP 栈简介

LEMP 指代 Linux、Nginx、MySQL、PHP,是一个实现 web 服务器的栈,之所以简写为 LEMP 而不是 LNMP,因为Nginx 的读音同 Engine X,因此简写选的是 E 而不是 N,此外 LEMP 是实际可拼读的英文,而 LNMP 只能逐个字母发音(当然也有 LNMP 的简写,但个人比较支持 LEMP)。常见的搭建 web 服务器的组合还有 LAMP,它是 LEMP 的前辈, LAMP 中用的 web 服务器软件 为 Apache。

0x01 实现

1. 环境搭建过程

目标环境

linux+nginx+php- fpm+mysql

1.1 安装 Linux 系统

Linux 系统使用 Ubuntu-18.04 版本,使用 VMware Workstation 安装其镜像,Ubuntu 镜像在其官网上下载。

sudo apt-get update & sudo apt-get upgrade

1.2 安装 Nginx

在 Ubuntu 中安装 Nginx,只需要在终端输入以下命令即可:

sudo apt-get install nginx

在浏览器中浏览 nginx 默认网页,http://主机地址,说明安装成功

1.3 安装 Php-fpm

在终端输入以下命令即可:

sudo apt-get intall php-fpm

查看服务是否运行:

systemctl status php7.2-fpm.service 

1.4 安装 Mysql

在终端输入以下命令安装 mysql 和 php 的 mysql 支持:

sudo apt-get install mysql-server mysql-client php-mysql

安装完成后,检查 mysql 服务器是否运行:

sudo systemctl mysql.service

2. 配置自己的网站

目标

搭建本地网站 www.jaylen.com,配置 Nginx 使其支持 php,本地网站显示一张图片,图片下方有个评论界面,可以输入评论,显示到网页中,评论会存到 mysql 数据库中。

2.1 域名映射

要在本地网络中,通过浏览器访问到 www.jaylen.com,需要修改主机的 hosts 文件,在 /etc/hsots 添加一行:

your-ip-add www.jaylen.com

这样就不需要 DNS 服务器解析该域名了(域名并没有注册!!!,DNS 服务器是没有记录的)。

2.2 配置 Nginx

首先需要配置 Nginx,使其支持 php,在 /etc/nginx/conf.d 目录下添加自己网站服务器的配置,jaylen.com.conf ,内容如下:

 #  server configuration
 server {
     listen 80;
     listen [::]:80;
     server_name jaylen.com;
     root /var/www/jaylen.com; # 网站在主机的根目录
 
     # Add index.php to the list if you are using PHP
     index index.html index.htm index.nginx-debian.html index.php;
 
     location / {
         # First attempt to serve request as file, then
         # as directory, then fall back to displaying a 404.
         try_files $uri $uri/ =404;
     }
 
     # pass PHP scripts to FastCGI server
     location ~ \.php$ {
         include snippets/fastcgi-php.conf;
     
         # With php-fpm (or other unix sockets):
         fastcgi_pass unix:/run/php/php7.2-fpm.sock;
     }
 
 }

测试配置是否成功:

sudo nginx -t

成功的话,出现如下提示信息:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

重新加载配置:

sudo nginx -s reload

2.3 创建网站主页

首先为网站创建一个根目录 /var/www/jaylen.com ,用于存储网站内容,该目录和 Nginx 配置文件中的 root 指令的内容一致。

sudo mkdir /var/www/jaylen.com

写个简单的 html 页面和 php进行测试,index.html

<html>
     <body>
         <head>
             <title>
                 jaylen's homepage
             </title>
         </head>
         <h1>Welcome to Jaylen's Homepage</h1>
         <p>Website is under construction, wait...</p>
     </bdoy>
 </html>

index.php

<html>
     <body>
         <head>
             <title>
                 jaylen's homepage
             </title>
         </head>
         <h1>Welcome to Jaylen's Homepage</h1>
         <p>Website is under construction, wait...</p>
         <?php
             echo phpinfo();
         ?>
     </bdoy>
 </html>

通过浏览器浏览网站 www.jaylen.com

浏览 www.jaylen.com/index.php

浏览 www.jaylen.com/index.php

测试完成,没有问题。

进一步完善网页内容,使其显示一张图片,图片下方带有评论输入框,可以提交评论,提交的评论存到主机的 mysql 数据库中,而后显示在评论区,最后完成的大致如下图,简陋的不行。


在 mysql 中创建一个新用户,并授权 INSERT、SELECT 权限,评论内容需要存到数据库,并读取数据库内容。

mysql>
CREATE USER 'user101'@'localhost'
  IDENTIFIED BY 'webdev101@Webdev102';
GRANT INSERT,SELECT
  ON *.*
  TO 'user101'@'localhost'
  WITH GRANT OPTION;

刷新授权表,退出,

mysql> FLUSH PRIVILEGE;
mysql> quit;

以新创建的用户重新登陆

mysql -u user101 -p

创建一个数据库

mysql> create database webdb101

切换到该数据库

mysql> use webdb101

创建一个表格,用于存放评论内容

mysql> CREATE TABLE comments (
id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
comment VARCHAR(200) NOT NULL
);

插入一条数据

mysql> INSERT INTO comments(comment) VALUES ('test');

最终的 index.php 文件内容如下:

<!DOCTYPE HTML>  
 <html>
 <head>
 <style>
 .error {color: #FF0000;}
 img {
     width: 30%;
     height: auto;
 }
 td {
     border: 1px solid black;
 }
 </style>
 </head>
 <body>  
 
 <?php
 // define variables and set to empty values
 
 include('connect-mysql.php');
 
 $comment = ""; 
 $commentErr = ""; 
 
 if ($_SERVER["REQUEST_METHOD"] == "POST") {
   if (empty($_POST["comment"])) {
     $commentErr = "You must write someting...";
   } else {
     $comment = test_input($_POST["comment"]);
     $sql = "INSERT INTO comments (comment) VALUES ('".$comment."')";
     if (mysqli_query($dbcon, $sql)) {
         $commentErr = "Submit comment successfully!";
     }   
     else {
         $commentErr = "Error:" . $sql . "<br>" . mysqli_error($dbcon);
     }   
   }
 }
 
 function test_input($data) {
   $data = trim($data);
   $data = stripslashes($data);
   $data = htmlspecialchars($data);
   return $data;
 }
 ?>
 
 <h1>Jaylen's HomePage</h1>
 <img src='image/homepage.jpeg' alt='homepage icon'> 
 <form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">  
   <hr>
   <p>Wirte your comment here!</p>
   <textarea name="comment" rows="5" cols="80"><?php echo $comment;?></textarea>
   <span class='error'>*<?php echo $commentErr; ?></span>
   <br></br>
   <input type="submit" name="submit" value="Submit">  
   <hr>
 </form>
 <?php
 $sqlget = "SELECT * FROM comments";
 $sqldata = mysqli_query($dbcon, $sqlget) or die("Fail to connect to database!" .  mysqli_error($dbcon));
 
 echo "<table>";
 echo "<tr><th>Comments</th></tr>";
 
 while($row = mysqli_fetch_array($sqldata, MYSQLI_ASSOC)) {
     echo "<tr><td>";
     echo $row['comment'];
     echo "</td></tr>";
 }
 
 echo "</table>";
 ?>
 </body>
 </html>

连接数据库的操作,单独放到 connec-mysql.php 文件中。

在网页中测试下提交评论,

[外链图片转存失败(img-4GV7Mzuw-1565152977001)(https://cdn.jsdelivr.net/gh/BingSlient/WebSecurityLearning/WebSecurityBasics/images/1564841954745.png)]

2.4 问题

记录搭建自己本地网络过程中遇到的问题。

  1. 配置 Nginx 使其支持 php 时,访问 http://localhsot/index.php,出现 502 Bad Gateway 错误,查看 Nginx 错误日志 /var/log/nginx/error.log ,找到错误记录如下:

    2019/08/02 03:11:53 [crit] 4293#4293: *50 connect() to unix:/var/run/php/php7.0-fpm.sock failed (2: No such file or directory) while connecting to upstream, client: 192.168.47.129, server: _, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.0-fpm.sock:", host: "192.168.47.129"
    

    原因是找不到 unix:/var/run/php/php7.0-fpm.sock文件,而我安装的版本是 7.2 的,因此应该是配置错误了,在系统中查找 php7.0-fpm.sock 发现实际路径为 /run/php/php7.0-fpm.sock 修改 /etc/nginx/sites-avalable/default 文件如下:

            # pass PHP scripts to FastCGI server
    
            location ~ \.php$ {
                    include snippets/fastcgi-php.conf;
    
                    # With php-fpm (or other unix sockets):
                    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
                    # With php-cgi (or other tcp sockets):
                    #fastcgi_pass 127.0.0.1:9000;
            }
    
  2. index.php 文件中对数据库进行插入操作时,提示错误:

    Unknown column '' in 'field list'
    

    原因时插入的数据是 VARCHAR 类型,需要额外添加双引号,我原来的 sql 语句如下:

    $sql = "INSERT INTO comments (comment) VALUES ($comment)";
    

    这样实际导致 $comment 变量展开后是一个标识符,而不是字符串,需要在外面添加额外的双引号,具体处理如下,注意在单引号中 $ 不会被识别成特殊字符,因此不会展开变量,而双引号中不能直接包含双引号,所以就成了下面的结果。

    $sql = "INSERT INTO comments (comment) VALUES ('".$comment."')";
    

3.本地服务器加固

3.1 加固 Linux 系统

时刻保持系统和软件为最新版本

在 Ubuntu 中检查系统需要更新的软件包,使用如下命令,apt-get -s 命令用于模拟后面命令的操作,但实际不会改变系统的状态,所以 apt-get -s upgrade 只会模拟软件更新的过程,你会看到被更新的软件的信息,但实际并没有更新到系统上 :

sudo apt-get update && sudo apt-get -s upgrade

然后根据需要更新你想要更新的软件。如果你想更新所有软件,使用如下命令:

sudo apt-get update && sudo apt-get upgrade

加固远程登陆

在使用和管理服务器时,往往我们需要远程登陆服务器,这就需要我们保证远程登陆过程的安全性。以下步骤一定程度提高了远程登陆的安全性。

  1. 强制使用高强度用户密码(数字、字母、字符的组合且长度14位以上)
  2. 更改 SSH 默认的端口(22)为随机端口
  3. 禁止 root 身份的远程登陆
  4. 使用公钥认证机制进行远程登陆
  5. 使用 Linux 标准用户而不是 root 用户执行上述操作,并且该用户的权限可提升成 root 权限

现以 Ubuntu 系统为例,完成上述操作:

强制使用高强度用户密码

要强制用户使用高强度密码,需要安装额外的模块 libpam-cracklib

sudo apt-get install libpam-cracklib

在 Ubuntu 中,密码策略(规定密码的长度,字符等)定义在 /etc/pam.d/common-password 文件中,如果要规定,密码长度为 14,包含大小写字符数字和字符,在文件中,在 pam_unix.so的前一行,添加:

password required pam_cracklib.so try_first_pass retry=3 minlen=14 lcredit=-1 ucredit=-1 dcredit=-1 ocredit=-1 difok=2 reject_username

上述配置的选项的描述如下,详情参考 libpam-cracklib 文档

选项描述
retry=N设置密码时的,最大重试次数
minlen=N新密码的最小长度
lcredit=N最少小写字母数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
ucredt=N最少大写字母数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
dcredit=N最少数字的数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
ocredit=N最少其它字符数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
difok=N和旧密码不同的字符数
reject_username禁止用户名作为密码

总之规定各种字符类型的个数,要使用负数,其绝对值表示该类型字符至少有多少个。

更改 SSH 默认的端口(22)为随机端口

禁止 root 身份的远程登陆

修改 ssh-server 的配置文件:

sudo vim /etc/ssh/sshd_config 

找到 # Port 22 更改为:

Port 2019

找到 #PermitRootLogin,更改为:

PermitRootLogin no

远程登陆则需要指定该端口执行登陆,而且无法使用 root 用户登陆

sss username@ip-addr -p 2019

使用公钥认证机制进行远程登陆

SSH 登陆 提供公钥认证机制的登陆,即不需要密码的登陆方式,但是需要客户端生成公钥和私钥,并将公钥发送给服务端,服务端将公钥添加到相应用户的配置文件中。

首先客户端需要生成,密钥对:

ssh-keygen -t rsa -b 4096

使用默认文件名,一路 Enter 最后,生成的密钥对文件在 ~/.ssh/ 目录下,其中 id_rsa 为私钥,id_rsa.pub 为公钥。

接着使用将公钥文件上传到服务器:

ssh-copy-id username@ip-addr -p portnum

该命令会把公钥文件的内容,写入到 /home/username/ssh/authorized_keys 文件中,所以也可以手动添加内容。如此一来就可以 username 的身份,不使用密码登陆服务器了。

ssh -p 2019 username@ip-addr

3.2 加固 Nginx

防止信息泄露

Nginx 默认开启 Server Token(显示版本号),这样使得 Nginx 的版本号很容易被获取,如下图为连接域名不存在资源时的返回页面,可以看到 Nginx 的版本号


/etc/nginx/nginx.conf 中 http 块中添加(去掉注释即可):

server_tokens off	

关闭后,访问域名下不存在的资源,返回页面中没有了 Nginx 的版本号 信息。

增加访问控制策略

Nginx 可以使用 allowdeny 指令在配置文件中允许或禁止特定 IP 的访问, 编辑 Niginx 配置文件 /etc/nginx/conf.d/jaylen.com.conf,只允许 192.168.47.129 192.168.47.130 访问 网站 www.jaylen.com

 #  server configuration
 server {
     listen 80; 
     listen [::]:80;
 
     # IPs access control
     allow 192.168.47.129;
     allow 192.168.47.130;
     deny all;
         
     root /var/www/jaylen.com;

使用 TLS 加固 Nginx

TLS 可以加密客户端和服务端通信的数据,降低信息泄露的风险。对于本地网站可以使用 SSL 自签 证书 实现 HTTPS 连接。当然在公网中使用的网站,通常会使用 CA 认证的证书,要免费使用 SSL 证书,可参考:

How to Install Nginx with Let’s encrypt and get A+ from SSLLabs Test

要在 Nginx 配置自签证书,首先在配置目录下建一个文件夹,进到文件夹中:

sudo mkdir /etc/nginx/ssl/
cd /etc/nginx/ssl

生成密钥:

sudo openssl genrsa -aes256 -out nginx.key 1024

接着生成 CSR

sudo openssl req -new -key nginx.key -out nginx.csr
Enter pass phrase for nginx.key:
Can't load /home/jaylen/.rnd into RNG
140067713049024:error:2406F079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/home/jaylen/.rnd
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN 
State or Province Name (full name) [Some-State]:Guangdong
Locality Name (eg, city) []:Guangzhou
Organization Name (eg, company) [Internet Widgits Pty Ltd]:IT
Organizational Unit Name (eg, section) []:IT
Common Name (e.g. server FQDN or YOUR name) []:www.jaylen.com
Email Address []:jaylen@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:admin           
An optional company name []:IT

最后,签发证书:

sudo openssl x509 -req -days 365 -in nginx.csr -signkey nginx.key -out nginx.crt

成功签发:

ignkey nginx.key -out nginx.crt
Signature ok
subject=C = CN, ST = Guangdong, L = Guangzhou, O = IT, OU = IT, CN = www.jaylen.com, emailAddress = jaylen@gmail.com
Getting Private key
Enter pass phrase for nginx.key:

接着在 /etc/nginx/conf.d/jaylen.com.conf 中修改:

 server {
     listen 443 ssl;
     ssl_certificate /etc/nginx/ssl/nginx.crt;
     ssl_certificate_key /etc/nginx/ssl/nginx.key;
     ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;

重新加载 Nginx 配置:

sudo nginx -s reload

提示输入之前步骤设置的密码,输入即可。

其它 加固措施,可仔细阅读:

Top 25 Nginx Web Server Best Security Practices

Nginx Web Server Security and Hardening Guide

3.3 加固 mysql

运行 mysql_secure_installation 工具(安装 mysql 后自带的 shell 脚本),进行 mysql 的安全检查,

根据提示,设置密码为最高级别,并为 root 用户设置密码,最后同意以下选项:

  • Remove anonymous users? – 删除匿名用户
  • Disallow root login remotely? – 禁止远程使用 root 用户登陆
  • Remove test database and access to it? – 删除测试数据库和其访问权限
  • Reload privilege tables now? – 重载授权表

3.4 加固 PHP

修改 php.ini 文件

当文件不存在时停止 PHP 处理

Nginx 对于 PHP 支持的配置文件中常常会使用如下形式的配置,该配置使得 PHP 解释器接受所有以 .php结尾的 URI,这样一来就会存在很大的风险,存在任意代码执行漏洞,具体解释见参考资料 [6]。

修改 /etc/php/7.2/fpm/php.ini文件(不同系统该文件位置略有不同),设置cgi.fix_pathinfo=0,可以禁止 PHP 解释器查找文件系统中不存在的文件,使用sed 命令完成文件内容的修改:

sudo sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g' /etc/php/7.2/fpm/php.ini

禁用危险的 PHP 函数

php.ini 中添加:

disable_functions =exec,eval,phpinfo,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

限制文件上传功能

如果网站不需要文件上传功能,应该禁用。

file_uploads=Off

如果需要上传功能,则设置文件的大小,根据实际情况设置,如头像图片上传1M足矣。

file_uploads=On
upload_max_filesize=1M

设置 POST 方法传输数据的大小

POST 方法是当客户端需要向服务器发送数据时使用的,该方法可被用于对服务器进行 DoS 攻击等,所以需要将其能传输的数据大小设置成合理的数值,如果网站不需要上传文件等数据量大的操作,4KB也应该足够了。

post_max_size=1K

防止 PHP 信息泄露

expose_php = Off

限制 PHP 脚本的最长执行时间

# set in seconds
max_execution_time = 30 #最长执行时间 30 s
max_input_time = 30		#脚本解析输入最长时间30s
memory_limit = 40M 		#脚本最大使用内存40M

这样可以有效防止大规模的 DOS 攻击。

禁用未使用的 PHP 模块

查看已安装的 PHP 模块:

php -m

根据实际情况,禁用不使用模块,注释掉 php.ini相应的配置行。

0x02 参考资料

[1] Install a LEMP Stack on Ubuntu 18.04

[2] Serve PHP with PHP-FPM and NGINX

[3] Nginx vs Apache

[4] Setting up an Nginx Reverse Proxy

[5] Getting Started with NGINX

[6] Passing Uncontrolled Requests to PHP

[7] Unknown column '' in 'field list'解决方案

[8] What’s a LEMP stack?

[9] How to secure LEMP stack

[10] Use Public Key Authentication with SSH

[11] Top 25 Nginx Web Server Best Security Practices

[12] Nginx Web Server Security and Hardening Guide

BingSlient 个人博客文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值