分类 linux 下的文章

使用lego签发Let's Encrypt的证书

五年前Let‘s Encrypt还不支持泛域名证书,我这个懒人为了省事,一下子买了五年的wildcard证书。截止到2020年7月22日,Let’s Encrypt已经有近六千四百万活跃的域名,有一亿三千六百万个证书,每天签发近一百五十万次。最重要的是证书全部是免费的,经过这几年的发展,Let‘s encrypt已经是目前互联网最重要的组织之一。而且周边和教程都已经非常成熟了,客户端就有几十种。我的网站要求就两点,一是能支持签发和续签widlcard证书,第二就是要使用简单,最终综合比较了一下选择了Lego。

Lego是Go语言写的,使用之前要先安装GoLang。

一、安装GoLang
从官网下载go语言相应版本的客户端,解压安装即可,需要注意以下两点:

  1. Linux系统下不要安装多个版本的GO
  2. 需要正确设置GOROOT和环境变量。

不然编译lego会有很多错误,例如go的解压后的目录在/usr/local/go,设置如下:

export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin/

将这两条设置放到profile或者bash_profile中即可,和JDK设置类似,这里不需要多说。

二、编译Lego
编译就比较简单了,两条命令即可:

git clone https://github.com/go-acme/lego
cd lego && make build

等待执行完,编译好的文件在dist目录下,复制到PATH里就可以直接用了。

三、签发和续签证书
使用超级简单,签发和续签一条命令即可。泛域名证书只能通过dns验证,需要域名NS的api key,具体支持的ns厂商可以通过:

lego dnshelp

查看需要设置的参数:

lego dnshelp -c dnspod

签发需要先设置api,dnspod完整的API Token是由 ID,Token 组合而成,用英文的逗号分割:

export DNSPOD_API_KEY="ID_xxxx,Token_xxxxxx"
export DNSPOD_HTTP_TIMEOUT="300"

签发:

lego --email="will@nixops.me" --domains="nixops.me" --domains="*.nixops.me" --accept-tos --path=/tmp/lego/ --dns="dnspod"  --dns.resolvers="8.8.8.8" run

--run-hook: 签发成功后执行的命令或脚本

续签:

lego --email="will@nixops.me" --domains="nixops.me" --domains="*.nixops.me" --accept-tos --path=/tmp/lego --dns="dnspod" renew --reuse-key 

--reuse-key: 不重新生成私钥
--renew-hook: 续签成功后执行的命令或脚本,如替换证书,reload服务
--days: 可以指定提前续期的天数

四、自动签发和续签脚本

#!/bin/bash

lego_bin=/root/lego/lego
save_path=/usr/local/keys/
account_email="will@nixops.me"

export DNSPOD_API_KEY="ID_xxxx,Token_xxxxxx"
export DNSPOD_HTTP_TIMEOUT="300"


case $1 in

run)
    $lego_bin --email="$account_email" --domains="*.$2" --domains="$2" --accept-tos --path=$save_path --dns="dnspod"  --dns.resolvers="1.1.1.1" run --run-hook="`readlink -f $0` reload"  >>/var/log/letencrypt.log
;;

renew)
    for domain in `$lego_bin  --path=$save_path list -n` ; do
    $lego_bin --email="$account_email" --domains="*.$domain" --domains="$domain" --accept-tos --path=$save_path --dns="dnspod" renew --reuse-key --renew-hook="`readlink -f $0` reload" >>/var/log/letencrypt.log 
done
;;

reload)
    cp -f $save_path/certificates/*.crt /usr/local/openresty/nginx/conf/vhosts/keys/
    cp -f $save_path/certificates/*.key /usr/local/openresty/nginx/conf/vhosts/keys/
    rm -rf /usr/local/openresty/nginx/conf/vhosts/keys/*.issuer.crt
    /usr/local/openresty/bin/openresty -s reload
   ;;

*)
   echo "黙认签发泛域名证书,只写根域名即可"
   echo "证书保存在 $save_path"
   echo "用法: 1. 签发证书 $0 run nixops.me"
   echo "用法: 2. 续签证书 $0 renew "
   echo "用法: 3. 复制证书,reload服务 $0 reload"
   
;;
esac

将以上脚本加入crontab中,每天执行一次renew:

crontab -e
15 3  * * * /usr/local/bin/ssl.sh renew nixops.me >>/dev/null 2>&1

最后不得不说,DNSPOD、namesilo的ns太渣了,默认解析TTL太长导致dns验证的签发和续签都太慢,失败太常见了,成功才是偶然。付费的ns一般都比较稳定,例如aws的router53能在2分钟内签发完成,免费ns测试cloudflare不错,其它的没有测试过。

使用systemtap进行抓包

systemtap是一个监控追踪系统调用、调试内核的工具,可以在系统运行时动态的调试内核,可以说是内核开发者必须要掌握的工具。

我使用这个工具是因为线上的应用存在fastjson的漏洞,被黑客执入了木马,在骨干交换机上抓到异常流量后,确定了内网服务器IP,登录服务器后使用lsmod、ps、netstat、ss、lsof这些命令无法发现异常,甚至连tcpdump都无法抓到这个木马,猜测是rootkit木马加载了内核模块、实现了无文件、无进程,只有每20分钟一次的心跳包,想揪出来非常困难。

一、安装

编译是比较麻烦的,centos base源里就有systemtap,主要有三个依赖:

  1. kernel-debuginfo 在CentOS-Debuginfo源里,默认没启用
  2. elfutils
  3. kernel-devel ,和 elfutils 一样,bases源里就有,yum会自己解决依赖
    kernel-debuginfo和kernel-devel 需要保证和当前运行的内核小版本号一致

使用yum安装:

yum install systemtap  systemtap-devel systemtap-runtime
yum --enablerepo=base-debuginfo install kernel-debuginfo kernel-debuginfo-common kernel-devel 

如果base源里没有找到systemtap,可以试试centosplus源:

yum --enablerepo=centosplus install  systemtap  systemtap-devel systemtap-runtime

当然也可以安装时直接指定版本,建议用这种方式:

yum install -y kernel-devel-$(uname -r)
yum --enablerepo=base-debuginfo install -y kernel-debuginfo-$(uname -r) kernel-debuginfo-common-$(uname -m)-$(uname -r)

按上面命令装好后验证一下kernel-debuginfo、kernel-devel版本是否和内核一致:

rpm -qa |grep kernel

如果系统内有多个版本的内核,建议卸载多余的内核、kernel-headers、kernel-devel和kernel-firmware,不然运行systemtap,容易出现错误。

二、systemtap脚本

目的是揪出来异常流量对应的进程、文件及文件位置,先来看看官网给的监控进程创建的脚本:

probe kprocess.create
{
    printf("%-25s: %s (%d) created %d\n",
    ctime(gettimeofday_s()), execname(), pid(), new_pid)
}

probe kprocess.exec
{
    printf("%-25s: %s (%d) is exec'ing %s\n",
    ctime(gettimeofday_s()), execname(), pid(), filename)
}

systemtap脚本执行需要root权限,将上述脚本保存为forktracker.stp,执行方法:

stap forktracker.stp 

可以将输出保存的文件:

stap -v forktracker.stp -o fork.log

改进一下,更详细一点:

probe kprocess.create {
     printf("%-25s: %s (%d:%d) created %d:%d\n",
         ctime(gettimeofday_s()), execname(), pid(), tid(), new_pid, new_tid)
}

probe kprocess.exec {
  printf("%-25s: %s (%d) is exec'ing %s\n",
     ctime(gettimeofday_s()), execname(), pid(), filename)
}

在来一个监控目的端口为443的流量。

cat tcp.stp 

#! /usr/bin/env stap
probe syscall.connect {
if(uaddr_ip_port=="443"){
    printf("ip:%s port:%s cmd:%s pid:%d ppid:%d\n",uaddr_ip, uaddr_ip_port,execname(),pid(),ppid())
    }
}

执行stap tcp.stp,然安在另外一个窗口执行 telnet 1.1.1.1 53进行测试,输出

ip:1.1.1.1 port:53 cmd:telnet pid:14934 ppid:10130

上面的信息不够详细,在来一个输出更详细的版本:

#! /usr/bin/env stap

probe syscall.connect {
    if(uaddr_ip_port=="1521"){
        printf("Time:%s remote_ip:%s remote_port:%s local_cmd:%s pid:%d local_pcmd:%s ppid:%d euid:%d egid:%d env_PWD:%s  \n",
           tz_ctime(gettimeofday_s()),uaddr_ip, uaddr_ip_port,execname(),pid(),pexecname(),ppid(),euid(),egid(),env_var("PWD"))
    }
}

执行 telnet 8.8.8.8 1521进行测试,输出

Time:Tue Jul 21 15:34:50 2020 HKT remote_ip:8.8.8.8 remote_port:1521 local_cmd:telnet pid:23892 local_pcmd:bash ppid:10130 euid:500 egid:500 env_PWD:/home/tomcat

上面两个只能监控tcp流量,在改进一下监控目的端口,不区分协议的,监控从本机出去到53端口的流量的进程

probe netfilter.ip.local_out {
  if (dport == 53)
      printf("%s[%d] %s:%d\n", execname(), pid(), daddr, dport)
      printf("Time:%s remote_ip:%s remote_port:%s local_cmd:%s pid:%d local_pcmd:%s ppid:%d euid:%d egid:%d env_PWD:%s  \n",
           tz_ctime(gettimeofday_s()),uaddr_ip, uaddr_ip_port,execname(),pid(),pexecname(),ppid(),euid(),egid(),env_var("PWD"))
    
}

输出

telnet[28032] 1.1.1.1:53
Time:Tue Jul 21 15:34:50 2020 HKT remote_ip:1.1.1.1 remote_port:53 local_cmd:telnet pid:28032 local_pcmd:bash ppid:10130 euid:500 egid:500 env_PWD:/home/tomcat

通过上面的操作基本上就确定了木马的文件名,接下来就是清理木马。centos单用户模式是维护模式,类似windows的安全模式,正常只会加载系统启动所需的最少服务,木马未针对单用户模式进行处理,所以在单用户模式下没有加载隐藏进程的内核模块,重启进入系统后直接find文件名清理即可。

参考文章:
https://sourceware.org/systemtap/documentation.html
https://sourceware.org/systemtap/examples/

tomcat多功能启动脚本

tomcat bin目录下已经有了启动、关闭的脚本,写这个脚本主要是为了方便自动化运维,把tomcat和jdk推到服务器上解压后,把这个脚本传到PATH目录下,改个名字,给个执行权限就可以了,可以少设置很多东西,同时功能更加丰富。主要有以下几点功能:

  1. 只需配置JDK目录和tomcat目录即可,无需设置全局JDK环境变量和setenv.sh
  2. 可以指定tomcat启动运行的用户
  3. 系统中有多个tomcat时,可以公用一个脚本
  4. 每次启动tomcat都会先检查目录权限和清理临时目录,并调整ulimit,防止应用使用缓存和出现性能问题
  5. 可以查看tomcat运行详细情况,cpu/内存/RSS/VSZ/运行时间等
  6. 可以使用本脚本tail输出 catalina.out
  7. 安全停止tomcat,先正常关闭tomcat,等待一段时间后kill,确保tomcat能够关闭
  8. 可以在tomcat关闭时,清理缓存目录temp/webapp/work
  9. tomcat运行时,可以使用jinfo/jmap/jstack导出jvm信息

都在脚本里,详细的自己看脚本吧。可以实现tomcat start/stop/status/kill/tail/clean/dump等功能。

#!/bin/bash
# Author: Will
# WebSite: https://www.nixops.me
# description: Tomcat multi-function script

 
#tomcat install dir
export CATALINA_HOME=/opt/tomcat-9.0_blog/

#TOMCAT_USER is the default user of tomcat
export TOMCAT_USER=tomcat

#SHUTDOWN_WAIT is wait time in seconds for java proccess to stop
SHUTDOWN_WAIT=5


# add java JDK path
export JAVA_HOME=/opt/jdk1.8
export CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

#####以上是按需修改的地方######

#TOMCAT_USAGE is the message if this script is called without any options
TOMCAT_USAGE="用法: $0 {\e[00;32mstart\e[00m|\e[00;31mstop\e[00m|\e[00;31mkill\e[00m|\e[00;32mstatus\e[00m|\e[00;31mrestart\e[00m|\e[00;32mtail\e[00m|\e[00;32mclean|\e[00;31mdump\e[00m}\n\
参数模式: $0 {\e[00;32m[action] \e[00;31m[tomcat path] \e[00;31m[run user] \e[00m} \n\n


启动参数说明: \n
start: 启动tomcat \n
stop: 正常停止tomcat \n 
kill: 杀掉tomcat进程  \n
status: 查看状态,是否运行/详细的运行状态  \n
restart: 调用stop/start重启tomcat  \n
tail: 使用tail命令查看catalina.out日志 \n  
clean: tomcat关闭时,清理缓存目录temp/webapp/work \n
dump: 使用jinfo/jmap/jstack导出jvm信息 \n\n

参数模式主要供脚本调用,或者多个tomcat公用一个init脚本;CATALINA_HOME和TOMCAT_USER通过参数传入,脚本中无需指定,使用方法: \n
$0 启动参数  tomcat路径  运行用户 \n
$0 status /opt/tomcat8/  tomcat \n

"

#Check init script ARGS
if [ "$#" -eq "3" ] ;then
    echo -e "\e[00;32mInit Script Run With Args \e[00m"
    export CATALINA_HOME=$2
    export TOMCAT_USER=$3
  elif [ "$#" -ne "1"  ] ;then
    echo -e $TOMCAT_USAGE
    exit 1
fi

#tomcat work dir
export CATALINA_BASE=$CATALINA_HOME

#Set init Run Env
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin:$CATALINA_HOME/bin
export PATH

tomcat_pid() {
  echo `ps -fe | grep $CATALINA_BASE |grep java | grep -v grep | tr -s " "|cut -d" " -f2`

}

tomcat_start_set() {

  if [ `user_exists $TOMCAT_USER` -eq "0" ];
  then
     echo -e "\e[00;31mSystem User:  $TOMCAT_USER not exist \e[00m"
     exit 1 
  elif [ `whoami` !=  "$TOMCAT_USER" ] &&  [ `whoami` != "root" ];
  then
     echo -e "\e[00;31mMust Run as : root or  $TOMCAT_USER \e[00m"
     exit 1 
     
  fi

  chown -R $TOMCAT_USER $CATALINA_HOME $CATALINA_BASE
  echo -e "\e[00;32mSet CATALINA_HOME CATALINA_BASE Permission Done\e[00m"

  if [ ! -z "$CATALINA_BASE" ]; then 
       \rm -rf $CATALINA_BASE/temp/* $CATALINA_BASE/work/* $CATALINA_BASE/webapps/* 
       echo -e "\e[00;32mClean Tomcat work/temp/webapps Done\e[00m"
  fi

  ulimit -n 100000
  umask 007
}
 
start() {
  pid=$(tomcat_pid)
  if [ -n "$pid" ]; then
    echo -e "\e[00;31mTomcat is already running (pid: $pid)\e[00m"
  else
    echo -e "\e[00;32mStarting Tomcat : $CATALINA_HOME\e[00m"

    tomcat_start_set 

    if [ `whoami` = "root" ]
    then
      /bin/su $TOMCAT_USER -c "cd $CATALINA_HOME/bin ; /bin/bash startup.sh"
    else
      cd $CATALINA_HOME/bin ; /bin/bash startup.sh
    fi

    status
  fi
  return 0
}
 
status(){
  pid=$(tomcat_pid)
  if [ -n "$pid" ]; then 
    echo -e "\e[00;32mTomcat is running with pid: \e[00;31m$pid\e[00m"
    ppid=`ps --no-headers  -p $pid -o ppid`
    cpu=`ps --no-headers  -p $pid -o %cpu`
    mem=`ps --no-headers  -p $pid -o %mem`
    user=`ps --no-headers  -p $pid -o user`
    group=`ps --no-headers  -p $pid -o group`
    stime=`ps --no-headers  -p $pid -o stime`
    etime=`ps --no-headers  -p $pid -o etime`
    rss=`ps --no-headers  -p $pid -o rss`
    vsz=`ps --no-headers  -p $pid -o size`
    cmd=`ps --no-headers  -p $pid -o cmd`
    echo -e "\e[00;32mPPID: \e[00;31m$ppid \e[00m"
    echo -e "\e[00;32mRun User: \e[00;31m$user \e[00m"
    echo -e "\e[00;32mRun Group: \e[00;31m$group \e[00m"
    echo -e "\e[00;32mCurrent Tomcat CPU Usage: \e[00;31m$cpu% \e[00m"
    echo -e "\e[00;32mCurrent Tomcat Mem Usage: \e[00;31m$mem% \e[00m"
    echo -e "\e[00;32mRSS Mem: \e[00;31m$rss KB \e[00m"
    echo -e "\e[00;32mVSZ Mem: \e[00;31m$vsz KB \e[00m"
    echo -e "\e[00;32mStart Time: \e[00;31m$stime \e[00m"
    echo -e "\e[00;32mTotal Run Time: \e[00;31m$etime \e[00m"
    echo -e "\e[00;32mStart Cmd: \e[00;31m$cmd \e[00m"

  else 
    echo -e "\e[00;31mTomcat is not running\e[00m"
  fi
}

terminate() {
  pid=$(tomcat_pid)
  if [ -n "$pid" ]; then
     echo -e "\e[00;31mPid: $pid,Terminating Tomcat\e[00m"
     kill -9 $(tomcat_pid)
  else
     echo -e "\e[00;31mTomcat Pid Not Found,Skip Kill !\e[00m"
  fi 
  
}

stop() {
  pid=$(tomcat_pid)
  if [ -n "$pid" ]; then
    echo -e "\e[00;31mStoping Tomcat\e[00m"
    sh $CATALINA_HOME/bin/shutdown.sh
 
    let kwait=$SHUTDOWN_WAIT
    count=0;
    until [ `ps -p $pid | grep -c $pid` = '0' ] || [ $count -gt $kwait ]
    do
      echo -n -e "\n\e[00;31mwaiting for processes to exit\e[00m";
      sleep 1
      let count=$count+1;
    done
 
    if [ $count -gt $kwait ]; then
      echo -n -e "\n\e[00;31mkilling processes didn't stop after $SHUTDOWN_WAIT seconds\e[00m"
      terminate
    fi
  else
    echo -e "\e[00;31mTomcat is not running\e[00m"
  fi
 
  return 0
}
 
user_exists(){
  if id -u $1 >/dev/null 2>&1; then
    echo "1"
  else
    echo "0"
  fi
}
 
tail_log(){
  pid=$(tomcat_pid)
  if [ -n "$pid" ]; then
     echo -e "\e[00;31mPid: $pid,run tail -f $CATALINA_BASE/logs/catalina.out\e[00m"
     tail -n 100 -f $CATALINA_BASE/logs/catalina.out
  else
     echo -e "\e[00;31mTomcat Pid Not Found,Skip Tail Log !\e[00m"
  fi
}

clean(){

  pid=$(tomcat_pid)
  if [ -n "$pid" ]; then
    echo -e "\e[00;31m Tomcat Running With Pid: $pid,Skip Clean\e[00m"
  else
    echo -e "\e[00;32mClean Tomcat logs and work/temp/webapps \e[00m"

    if [ ! -z "$CATALINA_BASE" ]; then
      \rm -rf $CATALINA_BASE/logs/* 
      echo -e "\e[00;32mClean Tomcat logs/* Done\e[00m"
      \rm -rf $CATALINA_BASE/temp/* $CATALINA_BASE/work/* $CATALINA_BASE/webapps/*
      echo -e "\e[00;32mClean Tomcat work/temp/webapps Done\e[00m"
    fi
  fi

}

dump(){
  pid=$(tomcat_pid)
  if [ -n "$pid" ]; then
    echo -e "\e[00;31m Tomcat Running With Pid: $pid,Start  dumping \e[00m"
    JAVA_HOME=`ps --no-headers  -p $pid -o cmd |awk -F bin '{print $1}'`

    if [  -n "$JAVA_HOME" ];then  
        echo -e "\e[00;31m jinfo Dump pid : $pid to /tmp/tomcat_"$pid"_jinfo_`date -I`.log \e[00m"
        $JAVA_HOME/bin/jinfo  $pid > /tmp/tomcat_"$pid"_jinfo_`date -I`.log

        echo -e "\e[00;31m jstack Dump pid : $pid to /tmp/tomcat_"$pid"_jstack_`date -I`.log \e[00m"
        $JAVA_HOME/bin/jstack  -F $pid > /tmp/tomcat_"$pid"_jstack_`date -I`.log

        echo -e "\e[00;31m jmap -heap Dump pid : $pid to /tmp/tomcat_"$pid"_jmap_heap_`date -I`.log \e[00m"
        $JAVA_HOME/bin/jmap  -heap -F $pid > /tmp/tomcat_"$pid"_jmap_heap_`date -I`.log 

        echo -e "\e[00;31m jstat -gcutil Dump pid : $pid to /tmp/tomcat_"$pid"_jstat_gcutil_`date -I`.log \e[00m"
        $JAVA_HOME/bin/jstat -gcutil $pid > /tmp/tomcat_"$pid"_jstat_gcutil_`date -I`.log 

        echo -e "\e[00;31m jmap -histo Dump pid : $pid to /tmp/tomcat_"$pid"_jmap_histo_`date -I`.log ,this action will trigger GC First\e[00m"
        $JAVA_HOME/bin/jmap  -histo -F $pid > /tmp/tomcat_"$pid"_jmap_histo_`date -I`.log 

        echo -e "\e[00;31m jmap -clstats Dump pid : $pid to /tmp/tomcat_"$pid"_jmap_clstats_`date -I`.log , this action will suspend the app \e[00m"
        $JAVA_HOME/bin/jmap  -clstats  -F $pid > /tmp/tomcat_"$pid"_jmap_clstats_`date -I`.log 

        echo -e "\e[00;31m jmap -dump Dump pid : $pid to /tmp/tomcat_"$pid"_jmap_dump_`date -I`.log , this action will suspend the app \e[00m"
        $JAVA_HOME/bin/jmap -dump:live,format=b,file=/tmp/tomcat_"$pid"_jmap_clstats_`date -I`.log  $pid  
          
    else
        echo -e "\e[00;31m Can't found jinfo/jstack/jmap/jstat  under $JAVA_HOME/bin/ , Skip Dump \e[00m"
    fi
    
  else
    echo -e "\e[00;32mTomcat not Running. Skip Dump! \e[00m"
  fi

}

case $1 in
  start)
    start
  ;;
  stop)  
    stop
  ;;
  restart)
    stop
    start
  ;;
  status)
    status
  ;;
  kill)
    terminate
  ;;    
  tail)
    tail_log
  ;;    
  clean)
    clean 
  ;;    
  dump)
    dump
  ;;
  *)
    echo -e $TOMCAT_USAGE
  ;;
esac

exit 0

nginx两种实用的自定义访问日志格式

第一种 程序友好型json格式:

log_format json_format  escape=json
    '{"@timestamp":"$time_iso8601",'
    '"client_ip":"$remote_addr",'
    '"client_region":"$ipdb_raw",'
    '"request_method":"$request_method",'
    '"request_scheme":"$scheme",'
    '"request_host":"$host",'
    '"request_uri":"$request_uri",'
    '"http_referer":"$http_referer",'
    '"send_body_size":"$body_bytes_sent",'
    '"http_status":"$status",'
    '"is_completion":"$request_completion",'
    '"request_time":"$request_time",'
    '"user_agent":"$http_user_agent",'
    '"XFF":"$http_x_forwarded_for",'
    '"request_protocol":"$server_protocol",'

    '"upstream_name":"$proxy_host",'
    '"upstream_addr":"$upstream_addr",'
    '"upstream_response_time":"$upstream_response_time",'
    '"upstream_status":"$upstream_status",'
    '"upstream_response_length":"$upstream_response_length",'
    '"upstream_connect_time":"$upstream_connect_time",'
    '"upstream_cache_status":"$upstream_cache_status",'
    '"upstream_bytes_sent":"$upstream_bytes_sent",'
    '"upstream_bytes_received":"$upstream_bytes_received",'

    '"nginx_host":"$hostname",'
    '"ssl_protocol":"$ssl_protocol",'
    '"ssl_cipher":"$ssl_cipher",'
    '"request_id":"$request_id"}';

json格式对程序友好,配合ELK等日志采集、分析系统使用很方便,方便对日志进行深度分析。但是多数人平时比较喜欢直接查看日志文件,这时json格式的日志文件看起来就不够清晰了,日志较长,冗余信息较多,使用shell命令进行统计和分析也不方便。

第二种 运维友好型自定义格式:

log_format  main  escape=json
    '$remote_addr |$ipdb_raw |[$time_local] |$host |$request |$status |BodySent:$body_bytes_sent |ReqTime:$request_time |$request_completion |$http_x_forwarded_for |$proxy_host |$upstream_addr |$upstream_status |$upstream_cache_status |UpResTime:$upstream_response_time |UpConnTime:$upstream_connect_time |UpResLen:$upstream_response_length |$hostname-$request_id |$scheme |$request_body |$http_referer |$http_user_agent |$http_cookie';

这种格式使用“|”作为分隔符,日志打印也不是很长,方便使用AWK等命令进行统计

需注意以下几点:

  1. $ipdb_raw这个变量是ipip.net ip库的nginx模块,如果未使用这个模块会报错,去掉或换成GEOIP即可。
  2. escape=json 表示以json格式输出,高版本的Nginx已经支持json格式,对于第二种自定义格式,开启这个参数能在access log中打印中文,不会乱码。
  3. $http_cookie是打印所有cookies,当然也可以打印session,对于高安全要求的情况,日志中不会允许泄漏用户cookies。但是某些情况需调试或打印非敏感的cookies,可以打印指定cookies,如打印nginx session sticky生产的cookies,假设名字叫backend: 'TRACE:$cookie_backend'。
  4. $hostname-$request_id 如果有做全局调用链分析的需求,这个参数可以做为全局的UUID,nginx对每个请求都生成一个唯一的UUID,传给后端工程打印出来即可。

linux限制rm命令

网上铺天盖地的说rm命令危险,但是真正想限制rm命令时,发现一个能用的解决方案都没有。常规做法是新建一个trash目录,把删除的内容mv到这个目录,在定期清理这个目录。思路是一样的,我自己动基于alias写了个脚本来限制rm命令,有以下好处:

  1. 只有一个脚本批量部署非常方便,不需求安装第三方软件
  2. 每次删除时会提示删除的内容,删除是将要移除的文件mv到/tmp/.trash/当天日期下,同时会提示是否清理目录,相当于做了double check
  3. 限制删除内容:只允许删除文件和目录,对于特殊的软链接、设备等文件默认不允许删除
    其它功能懒得写了,使用方法将脚本丢到/etc/profile.d/即可。
#!/bin/bash
#author: will
#website: https://www.nixops.me

rm_cmd(){

arg_array=()
for var in $*
do
    if [ ! `echo "$var" |grep "^-"` ];then
      arg_array+=("$var")      
    fi 
done 

if [ "$#" -eq "0" ] ;then
    echo -e "\e[00;32mYou Are Using Security \"rm\" command \e[00m"
    return 0
elif [ ${#arg_array[@]} -eq 0 ];then
    echo -e "\e[00;32mYou Are Using Security \"rm\" command \e[00m"
    return 0
fi

echo -e "\033[00;31mYou are going to DELETE:  \033[00m"

list_array=()
for element in ${arg_array[@]}
do
   if [ -f $element ];then
       echo FILE: $element 
       list_array+=("$element")
   elif [ -d $element ];then 
       echo DIR: $element 
       list_array+=("$element")
   elif [ -S $element ];then 
       echo -e "\e[00;32mSOCKET: $element NOT Allow To Delete\e[00m"
       return 0 
   elif [ -p $element ];then 
       echo -e "\e[00;32mPIPE: $element NOT Allow To Delete\e[00m"
       return 0 
   elif [ -b $element ];then 
       echo -e "\e[00;32mBLOCK DEVICE: $element NOT Allow To Delete\e[00m"
       return 0 
   elif [ -c $element ];then 
       echo -e "\e[00;32mCHARACTER DEVICE: $element NOT Allow To Delete\e[00m"
       return 0 
   else
       echo -e "\e[00;32mNOT Exist: $element \e[00m"
       return 0 
   fi
done

read -n1 -p $'\033[00;31mAre you sure to DELETE [Y/N]? ' answer
case $answer in
Y | y)
      echo -e "\n"

      if [ ! -d "/tmp/.trash/`date -I`" ]; then
        mkdir -p /tmp/.trash/`date -I`
        chmod 777 /tmp/.trash/`date -I`
      fi

      for element in ${list_array[@]}
      do 
        echo -e "Deleting $element to /tmp/.trash/`date -I`"
        #/bin/rm --preserve-root     -rf  $element
        
        mv $element /tmp/.trash/`date -I`

        if [ $? -ne "0" ];then
          echo -e "\nDeleted FAILED"
          return 0
        fi
      done
      echo -e "\nDeleted FINISHED"

      read -n1 -p $'\033[00;31mFree Disk Space ?  [Y/N]? ' fanswer
      case $fanswer in

      Y | y)
          /bin/rm --preserve-root -rf /tmp/.trash/*
          echo -e "\n"
      ;;
      *)
          echo -e "\nFree Disk Space SKIPED"
          echo -e "\n"
      ;;
      esac
;;
*)
      echo -e "\nDelete SKIPED"
;;

esac
}

alias rm='rm_cmd $@'
alias grep='grep --color=auto'
alias cp='cp -i'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias mv='mv -i'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'

使用dnspod api更改域名解析

有一堆在dnspod的域名要批量修改解析,虽然dnspod网页上可以批量修改,但是这些域名在不同的dnspod账户中,后续还会频繁修改解析,考虑到后续的工作量,还是写个脚本省事一点。dnspod的api还是比较简单的,api文档地址: http://www.dnspod.cn/docs/index.html
python 脚本:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = 'will'
# website: https://www.nixops.me

import sys,requests,json

class Dnspod:
    def __init__(self,token):
        self.token = token

    def getDomainList(self):
        postdata = {'login_token': self.token,'format':'json'}
        with requests.session() as start:
            login = start.post('https://dnsapi.cn/Domain.List',postdata)
            return login.text

    def getSub(self,domain):
        if len(domain.split('.')[0:-2]) == 0:
            return '@',str(domain)
        else:
            sub = ''
            for i in domain.split('.')[0:-2]:
            sub += i+ '.'
            return sub[0:-1], domain.split('.')[-2]+'.'+domain.split('.')[-1]

    def getRecord(self,domain):
        sub,root=self.getSub(domain)

        postdata = {'login_token': self.token,'format':'json','domain':root,'sub_domain':sub}
        with requests.session() as start:
            login = start.post('https://dnsapi.cn/Record.List',postdata)
            return json.loads(login.text)['records'][0]['id'],json.loads(login.text)['records'][0]['value']

    def modRecord(self,domain,ip,record_type,record_line):
        sub,root = self.getSub(domain)
        record_id,value = self.getRecord(domain)
        
        postdata = {'login_token': self.token,'format':'json','domain':root,'sub_domain':sub,'record_id':record_id,'value':ip,'record_type':record_type,'record_line':record_line}
        with requests.session() as start:
            login = start.post('https://dnsapi.cn/Record.Modify',postdata)
            if json.loads(login.text)['status']['code'] == '1':
                return 'success'
            else:
                return json.loads(login.text)

    def change(self,domain,ip,record_type,record_line):
       sub,root = self.getSub(domain)
       all_domains=[]
       for domain_name in  json.loads(self.getDomainList())['domains']:
           all_domains.append(domain_name['name'])
       if root not in all_domains:
           print '账户中未找到该域名'
           sys.exit(0)
       print '域名: '+ domain +  ' 当前解析: ' + str(self.getRecord(domain)[1]) +  ' 更改解析为: ' + ip + ' 记录类型: ' + record_type + ' 线路: ' + record_line  +' 状态: ' +str(self.modRecord(domain,ip,record_type,record_line))
if __name__ == '__main__':

    token="12345,7d9s9d00w8s7d7dko20394l04r23"

    Dnspod(token).change('www.nixops.me','8.8.8.8','A','默认')

使用PROXY protocol获取客户IP

获取客户IP是常见的需求,对于大流量的项目都会使用反向代理、负载均衡等,甚至多重代理,导致架构和网络都比较复杂,在这种情况下获取IP就不那么容易了。七层代理可以通过添加头信息来实现,如http协议的X-Forword-For,还比较方便;四层代理基本无法简单的获取到客户端IP地址,像LVS的FULLNAT模式,前端LVS把真实IP写在TCP option里面,后端服务器用内核toa模块获取客户IP;haproxy配合TPROXY也是类似的方式实现,两者都需编译内核非常麻烦,这种情况下就可以考虑使用代理协议

一、代理协议简介

代理协议即 PROXY protocol,是haproxy的作者Willy Tarreau于2010年开发和设计的一个Internet协议,通过为tcp添加一个很小的头信息,来方便的传递客户端信息(协议栈、源IP、目的IP、源端口、目的端口等),在网络情况复杂又需要获取客户IP时非常有用。如:

  • 多层NAT网络
  • TCP代理(四层)或多层tcp代理
  • https反向代理http(某些情况下由于Keep-alive导致不是每次请求都传递x-forword-for)

代理协议分为v1和v2两个版本,v1人类易读,v2是二进制格式,方便程序处理。Proxy protocol是比较新的协议,但目前已经有很多软件支持,如haproxy、nginx、apache、squid、mysql等等,要使用proxy protocol需要两个角色sender和receiver,sender在与receiver之间建立连接后,会先发送一个带有客户信息的tcp header,因为更改了tcp协议,需receiver也支持proxy protocol,否则不能识别tcp包头,导致无法成功建立连接。

二、nginx配置示例

NGINX版本建议不要低于1.9.10,否则对proxy protocol支持不好,很多bug,nginx前端sender直接配置tcp 转发443,不需要配置证书:

stream {
    upstream nixops_https {
        server nixops.me.backend.ip:8443;
    }

server {
    listen 443  ;
    proxy_connect_timeout 1s;
    proxy_timeout 3s;
    proxy_pass nixops_https;
    proxy_protocol on;
    }
}

后端receiver nginx配置所有https证书:

server {
    listen 8443  ssl proxy_protocol default;
    server_name
    $host
   ;

   ssl on;
   ssl_certificate     "/usr/local/nginx/conf/keys/nixops.me.crt";
   ssl_certificate_key "/usr/local/nginx/conf/keys/nixops.me.key";

location / {
        proxy_redirect         off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://127.0.0.1;
    }
}

配置后用http是访问前端直接就能看到绿锁了,也能直接拿到客户IP,很方便

三 、haproxy 配置示例

haproxy是proxy protocol的亲爹,支持和使用都非常,就send-proxy和accept-proxy两个参数即可,haproxy编译:

make TARGET=linux2628 USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1

配置文件

global
    maxconn 100000

defaults
    mode http
    timeout connect 10s
    timeout client 10s
    timeout server 10s

frontend nixopsfront
    mode tcp
    bind :443
    #bind *:443 accept-proxy 作为receiver
    default_backend nixopsbackend

backend nixopsbackend
    mode tcp
    balance roundrobin
    server s1 8.8.8.8:443 send-proxy  check
    #server s2 8.8.8.8:443 send-proxy-v2  check 作为sender

启动 :

./haproxy -d  -f ssl.cfg

参考文章:
https://www.nginx.com/resources/admin-guide/proxy-protocol/
http://blog.haproxy.com/haproxy/proxy-protocol/

linux下手动产生kernel panic

Kernel panic是内核错误,是系统内核遇到无法处理的致命错误时才会产生的异常,一般长这样:

Kernel panic - not syncing: Fatal exception
........其它错误日志........

除此之外,还有以下几种常见的:

Kernel Panic - not syncing:Attempted to kill init !
Kernel panic - not syncing: Out of memory and no killable processes
Kernal Panic - Not syncing : VFS: unable to mount root fs on unknown-block (0,0)
kernel panic - not syncing: Attempted to kill the idle task!
kernel panic - not syncing: killing interrupt handler!    

一旦发生kernel panic 只能重启服务器,可以通过以下两种方式产生:

1. 通过SysRq产生kernel panic

SysRq是magic SysRq key的简写,只要内核没挂,可以通过组合键来完成预先定义的系统操作。从而保证磁盘数据安全,避免数据丢失和文件系统检查,还可以收集CPU 内存和进程等系统信息,或者执行重新挂载文件系统、强制注销,关闭重启系统等操作,通常通过 Alt + SysRq(也就是print screen) + Command Key来执行,需要root权限,命令:

echo c > /proc/sysrq-trigger

'c' 是预定义的SysRq功能之一,触发Crashdump,产生kernel panic

如果是非root用户,需要打开sysrq功能:

echo 1 > /proc/sys/kernel/sysrq

或者:

sysctl -w kernel.sysrq=1 

2. 调用内核的panic()函数产生kernel panic

需要root权限,需要gcc、make和内核头文件:

      
yum install -y kernel-devel kernel-headers gcc make

创建软链接:

unlink  /lib/modules/`uname -r`/build
ln -s /usr/src/kernels/`uname -r`/ /lib/modules/`uname -r`/build 

然后通过下面命令产生kernel panic:

mkdir /tmp/kpanic && cd /tmp/kpanic && printf '#include <linux/kernel.h>\n#include <linux/module.h>\nMODULE_LICENSE("GPL");static int8_t* message = "buffer overrun at 0x4ba4c73e73acce54";int init_module(void){panic(message);return 0;}' > kpanic.c && printf 'obj-m += kpanic.o\nall:\n\tmake -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules' > Makefile && make && insmod kpanic.ko

以上命令编译一个调用了panic()函数的内核模块,并加载产生kenel panic,提醒下:不要在生产服务器上玩!!!

参考文章:

http://blog.csdn.net/maimang1001/article/details/19573487
https://unix.stackexchange.com/questions/66197/how-to-cause-kernel-panic-with-a-single-command

最新文章

最近回复

分类

归档

统计

  • 文章总数:168篇
  • 分类总数:5个
  • 评论总数:103条
  • 页面总数:172个
  • 本站运行:4870天

其它