20230806更新:本文后续更新内容将上传到个人笔记,请访问此处获取最新内容。
背景
由于服务器安全设定,只对外开放一个端口,如何提供ssh连接、https服务?搜索了下可以根据流量特征用sslh简单转发一下数据包到不同的内部端口。
sslh
在root下apt install sslh后修改配置:
# Default options for sslh initscript
# sourced by /etc/init.d/sslh
# binary to use: forked (sslh) or single-thread (sslh-select) version
# systemd users: don't forget to modify /lib/systemd/system/sslh.service
DAEMON=/usr/sbin/sslh
Run=yes
DAEMON_OPTS="--user sslh --listen 0.0.0.0:23456 --ssh 127.0.0.1:8864 --ssl 127.0.0.1:443 --http 127.0.0.1:8081 --pidfile /var/run/sslh/sslh.pid"
ssh的设定开了本地22和8864端口,配置时修改/etc/ssh/sshd_config文件,加一行Port 8864即可。同时记得使用公钥认证登录,禁用密码登录。nginx(1.22版本)的配置如下
nginx配置
user c01dkit;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server_tokens off;
server {
listen 8081;
listen 127.0.0.1:8081;
charset utf-8;
server_name xxxx.c01dkit.com;
if ($scheme = http ) {
return 301 https://$host:xxxx$request_uri;
}
error_page 404 /404.html;
}
server {
listen 127.0.0.1:443 ssl ;
listen 443 ssl ;
listen [::]:443 ssl ;
server_name xxxx.c01dkit.com;
charset utf-8;
ssl_certificate xxxx/fullchain.pem;
ssl_certificate_key xxxx/privkey.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root xxxxx;
index index.html index.htm;
error_page 404 /404.html;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME xxxx/www$fastcgi_script_name;
include fastcgi_params;
error_page 404 /404.html;
}
}
}
然后systemctl enable sslh、systemctl start sslh启动sslh。这里将本地23456端口收到的流量根据ssh、ssl、http的特征分别进行端口转发。此时接着设置防火墙将所有外部流量转发到23456端口即可。这里假定ssh服务也开在了8864端口,nginx配置https监听443端口、http监听8081端口。
iptables配置
iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 23456
这里假定外部端口开放的端口映射到本地22端口。这里22端口也是有ssh服务在监听。有时担心sslh服务挂掉导致23456没有ssh服务、ssh连不上,设置了定时任务来关掉、打开防火墙(此时只能ssh连接,提供运维窗口期),比如每周三4点到6点只提供22端口的ssh服务:
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
0 4 * * 3 iptables -t nat -D PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 23456
0 6 * * 3 iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 23456
由于这样设置iptables重启后会失效,所以服务器意外重启的话只不过是恢复到最基础的22端口ssh而已。
关于nginx,可以nginx -V查看编译选项,然后自己从源码编译下。常见的-V输出有:
nginx version: nginx/1.22.1
built by gcc 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04)
built with OpenSSL 3.0.2 15 Mar 2022
TLS SNI support enabled
configure arguments: --user=c01dkit --group=c01dkit --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module
这里指定user为c01dkit,然后网站也都放在c01dkit的家目录里面,以防网站页面因为权限问题打不开(好像默认是www-data),可能是蟹脚改法○( ^皿^)っ
https免费证书
关于https证书,可以按这里的方法,先snap install --classic certbot
安装certbot,(不知道为啥当时设置了一下certbot路径sudo ln -s /snap/bin/certbot /usr/bin/certbot
)。如果80端口已经对外开放,可以简单地certbot --nginx
自动帮忙认证(即certbot创建认证文件然后在公网访问)。如果80端口不对外开放,可以自选dns认证:certbot certonly --manual --preferred-challenges=dns
然后在域名管理那边添加一下记录即可。然后在nginx的conf那里设置好证书,访问就有https认证了!对于http访问,可以用301跳转。
证书90天就会过期,需要续期。使用上述方法续期时,如果直接使用certbot renew
会告知必须使用脚本来更新,即新增参数 --manual-auth-hook
。认证原理是这样的:renew的时候会生成一个随机的challenge存储在CERTBOT_VALIDATION
的全局变量里,需要在 --manual-auth-hook
执行的脚本里完成DNS TXT记录的更新,并使脚本返回0。当脚本完成后,certbot会检查DNS是否更新成了新的challenge值来判断是否有域名的解析权。
最直观的方法是certbot renew --manual-auth-hook=auth.sh
,然后auth.sh
里写上:
echo ${CERTBOT_VALIDATION} >> challenge.txt
sleep 120
exit 0
然后手动用challenge.txt
里面的内容在域名服务商那边的dns解析更新下txt记录。等到120秒后会认证成功,重置剩余证书有效时间到90天。
也可以写个用域名服务商提供的API自动化更新的脚本,比如这个(未测试)
需要先 pip install alibabacloud_alidns20150109
(这都2023年了咋还是用2015的版本)
# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import sys
import os
import ast
from typing import List
from alibabacloud_alidns20150109.client import Client as Alidns20150109Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_alidns20150109 import models as alidns_20150109_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
ACCESSKEYID = 'xxxxx'
ACCESSKEYTOKEN = 'xxxxx'
DOMAIN_NAME = 'xxxxx' # 比如c01kdit.com
CHALLEGNE = os.environ.get('CERTBOT_VALIDATION')
class AliDns:
@staticmethod
def create_client(
access_key_id: str,
access_key_secret: str,
) -> Alidns20150109Client:
"""
使用AK&SK初始化账号Client
@param access_key_id:
@param access_key_secret:
@return: Client
@throws Exception
"""
config = open_api_models.Config(
# 必填,您的 AccessKey ID,
access_key_id=access_key_id,
# 必填,您的 AccessKey Secret,
access_key_secret=access_key_secret
)
# 访问的域名
config.endpoint = f'alidns.cn-shanghai.aliyuncs.com'
return Alidns20150109Client(config)
def __init__(self):
self.client = None
def update(self, access_key_id, access_key_secret):
self.client = AliDns.create_client(access_key_id, access_key_secret)
def get_dns(self):
if self.client is None:
print('client is None')
return
describe_domain_records_request = alidns_20150109_models.DescribeDomainRecordsRequest(
domain_name=DOMAIN_NAME,
)
runtime = util_models.RuntimeOptions()
try:
# 复制代码运行请自行打印 API 的返回值
response = self.client.describe_domain_records_with_options(describe_domain_records_request, runtime)
return ast.literal_eval(response.body)
except Exception as error:
# 如有需要,请打印 error
print(error)
return None
if __name__ == '__main__':
# Sample.main(sys.argv[1:])
if CHALLEGNE is None:
print('CHALLEGNE is None')
sys.exit(1)
client = AliDns()
client.update(ACCESSKEYID, ACCESSKEYTOKEN)
records = client.get_dns()
if records is None:
print('records is None')
sys.exit(1)
for record in records['DomainRecords']['Record']:
if record['RR'] == '_acme-challenge':
update_domain_record_request = alidns_20150109_models.UpdateDomainRecordRequest(
record_id=record['RecordId'],
rr=record['RR'],
type=record['Type'],
value=CHALLEGNE,
)
runtime = util_models.RuntimeOptions()
try:
# 复制代码运行请自行打印 API 的返回值
response = client.client.update_domain_record_with_options(update_domain_record_request, runtime)
print(response.body)
except Exception as error:
# 如有需要,请打印 error
print(error)
sys.exit(1)