Zeppelin启用https过程和Hack内核以满足客户需求的记录。

原因是这客户很有意思,该客户中国分公司的人为了验证内网安全性,从国外找了一个***测试小组对Zeppelin和其他产品进行***测试,结果发现Zeppelin主要俩问题,一个是在内网没用https,一个是zeppelin里面可以执行shell命令和python语句。其实这不算大问题,zeppelin本来就是干这个用的。但是***小组不了解zeppelin是做什么的,认为即使在内网里,执行shell命令能查看操作系统的一些文件是大问题,然后发生的事就不说了,不是我们的问题了。


不过既然他们要求整改,我们也只好配合,虽然大家都觉得内网域名加https属于脱了裤子放屁,然后不让zeppelin干他本来应该干的事就更过分了,但鉴于客户是甲方,也只好hack源码了。


于是某个周末用了4个小时完成所有工作。


先记录下zeppelin加https访问,我们有自己的域名证书,所以直接用即可。如果没有域名证书,需要自签发,那么可以看第二部分,双向认证步骤。

https第一部分,已有域名添加jks:

openssl pkcs12 -export -in xxx.com.crt -inkey xxx.com.key -out xxx.com.pkcs12
keytool -importkeystore -srckeystore xxx.com.pkcs12 -destkeystore xxx.com.jks -srcstoretype pkcs12

https第二部分,自签发证书双向认证添加jks

# 生成root私钥和证书文件。
openssl genrsa -out root.key(pem) 2048 # Generate root key file
openssl req -x509 -new -key root.key(pem) -out root.crt # Generate root cert file
# 创建客户端私钥和证书以及证书请求文件csr
openssl genrsa -out client.key(pem) 2048 # Generate client key file
openssl req -new -key client.key(pem) -out client.csr # Generate client cert request file
openssl x509 -req -in client.csr -CA root.crt -CAkey root.key(pem) -CAcreateserial -days 3650 -out client.crt # Use root cert to generate client cert file
# 生成服务器端私钥,证书和证书请求文件csr
openssl genrsa -out server.key(pem) 2048 # Generate server key file, use in Zeppelin
openssl req -new -key server.key(pem) out server.csr @ Generate server cert request file
openssl x509 -req -in server.csr -CA root.crt -CAkey root.key(pem) -CAcreateserial -days 3650 -out server.crt # Use root cert to generate server cert file
# 生成客户端端jks文件
openssl pkcs12 -export -in client.crt -inkey client.key(pem) -out client.pkcs12 # Package to pkcs12 format, must input a password, you should remember the password
keytool -importkeystore -srckeystore client.pkcs12 -destkeystore client.jks -srcstoretype pkcs12 # The client password you just input at last step
# 生成服务器端jks文件
openssl pkcs12 -export -in server.crt -inkey server.key(pem) -out server.pkcs12 @ Package to pkcs12 format, must input a password, you should remember the password
keytool -importkeystore -srckeystore server.pkcs12 -destkeystore server.jks -srcstoretype pkcs12 # The server password you just input at last step

如果是不需要双向认证,只要单向自签发,不创建客户端的各种就可以了。

然后找个地把这些文件放过去,再修改zeppelin配置即可。

mkdir -p /etc/zeppelin/conf/ssl
cp server.crt server.jks /etc/zeppelin/conf/ssl

  zeppelin.server.ssl.port
  8443
  Server ssl port. (used when ssl property is set to true)


  zeppelin.ssl
  true
  Should SSL be used by the servers?


  zeppelin.ssl.client.auth
  false
  Should client authentication be used for SSL connections?


  zeppelin.ssl.keystore.path
  /etc/zeppelin/conf/ssl/xxx.com.jks
  Path to keystore relative to Zeppelin configuration directory


  zeppelin.ssl.keystore.type
  JKS
  The format of the given keystore (e.g. JKS or PKCS12)


  zeppelin.ssl.keystore.password
  password which you input on generating server jks step
  Keystore password. Can be obfuscated by the Jetty Password tool


然后反代那里也加上443的ssl证书以及443转8443的upstream即可。


然后是hack zeppelin源码加入关键字限制,这个确实找了一小会zeppelin发送执行源码给interpreter的地方,zeppelin架构比较清晰,但是代码挺复杂的,用到了很多小花活儿。比如thrift,interpreter脚本里建立nc监听。然后各个解释器插件用socket跟interpreter脚本通信,前端angular,后端jetty,还用shiro做验证和授权。回头可以单开好几篇说说zeppelin安装,使用和详细配置,做这项目基本把zeppelin摸透了。

找到发送前端编写内容给interpreter的java代码,然后用很生硬的办法限制执行命令。具体那个.java文件的名字我就不说了,有悬念有惊喜。我不写java,只负责读源码找到代码位置,hack的java是同事写的。然后编译,替换jar包,完成。后面改了改配置,后续的***测试顺利通过。

static HashSet blockedCodeString = new HashSet<>();
  static {
    blockedCodeString.add(new String[]{"import", "os"});
    blockedCodeString.add(new String[]{"import", "sys"});
    blockedCodeString.add(new String[]{"import", "subprocess"});
    blockedCodeString.add(new String[]{"import", "pty"});
    blockedCodeString.add(new String[]{"import", "socket"});
    blockedCodeString.add(new String[]{"import", "commands"});
    blockedCodeString.add(new String[]{"import", "paramiko"});
    blockedCodeString.add(new String[]{"import", "pexpect"});
    blockedCodeString.add(new String[]{"import", "BaseHTTPServer"});
    blockedCodeString.add(new String[]{"import", "ConfigParser"});
    blockedCodeString.add(new String[]{"import", "platform"});
    blockedCodeString.add(new String[]{"import", "popen2"});
    blockedCodeString.add(new String[]{"import", "copy"});
    blockedCodeString.add(new String[]{"import", "SocketServer"});
    blockedCodeString.add(new String[]{"import", "sysconfig"});
    blockedCodeString.add(new String[]{"import", "tty"});
    blockedCodeString.add(new String[]{"import", "xmlrpmlib"});
    blockedCodeString.add(new String[]{"etc"});
    blockedCodeString.add(new String[]{"boot"});
    blockedCodeString.add(new String[]{"dev"});
    blockedCodeString.add(new String[]{"lib"});
    blockedCodeString.add(new String[]{"lib64"});
    blockedCodeString.add(new String[]{"lost+found"});
    blockedCodeString.add(new String[]{"mnt"});
    blockedCodeString.add(new String[]{"proc"});
    blockedCodeString.add(new String[]{"root"});
    blockedCodeString.add(new String[]{"sbin"});
    blockedCodeString.add(new String[]{"selinux"});
    blockedCodeString.add(new String[]{"usr"});
    blockedCodeString.add(new String[]{"passwd"});
    blockedCodeString.add(new String[]{"useradd"});
    blockedCodeString.add(new String[]{"userdel"});
    blockedCodeString.add(new String[]{"rm"});
    blockedCodeString.add(new String[]{"akka "});
    blockedCodeString.add(new String[]{"groupadd"});
    blockedCodeString.add(new String[]{"groupdel"});
    blockedCodeString.add(new String[]{"mkdir"});
    blockedCodeString.add(new String[]{"rmdir"});
    blockedCodeString.add(new String[]{"ping"});
    blockedCodeString.add(new String[]{"nc"});
    blockedCodeString.add(new String[]{"telnet"});
    blockedCodeString.add(new String[]{"ftp"});
    blockedCodeString.add(new String[]{"scp"});
    blockedCodeString.add(new String[]{"ssh"});
    blockedCodeString.add(new String[]{"ps"});
    blockedCodeString.add(new String[]{"hostname"});
    blockedCodeString.add(new String[]{"uname"});
    blockedCodeString.add(new String[]{"vim"});
    blockedCodeString.add(new String[]{"nano"});
    blockedCodeString.add(new String[]{"top"});
    blockedCodeString.add(new String[]{"cat"});
    blockedCodeString.add(new String[]{"more"});
    blockedCodeString.add(new String[]{"less"});
    blockedCodeString.add(new String[]{"chkconfig"});
    blockedCodeString.add(new String[]{"service"});
    blockedCodeString.add(new String[]{"netstat"});
    blockedCodeString.add(new String[]{"iptables"});
    blockedCodeString.add(new String[]{"ip"});
    blockedCodeString.add(new String[]{"route "});
    blockedCodeString.add(new String[]{"curl"});
    blockedCodeString.add(new String[]{"wget"});
    blockedCodeString.add(new String[]{"sysctl"});
    blockedCodeString.add(new String[]{"touch"});
    blockedCodeString.add(new String[]{"scala.sys.process"});
    blockedCodeString.add(new String[]{"0.0.0.0"});
    blockedCodeString.add(new String[]{"git"});
    blockedCodeString.add(new String[]{"svn"});
    blockedCodeString.add(new String[]{"hg"});
    blockedCodeString.add(new String[]{"cvs"});
    blockedCodeString.add(new String[]{"exec"});
    blockedCodeString.add(new String[]{"ln"});
    blockedCodeString.add(new String[]{"kill"});
    blockedCodeString.add(new String[]{"rsync"});
    blockedCodeString.add(new String[]{"lsof"});
    blockedCodeString.add(new String[]{"crontab"});
    blockedCodeString.add(new String[]{"libtool"});
    blockedCodeString.add(new String[]{"automake"});
    blockedCodeString.add(new String[]{"autoconf"});
    blockedCodeString.add(new String[]{"make"});
    blockedCodeString.add(new String[]{"gcc"});
    blockedCodeString.add(new String[]{"cc"});
  }
  static boolean allMatch(String aim, String[] checker){
    if(checker == null || checker.length < 1){
      return false;
    }else {
      // by default, treat as match, every not match change it
      for (String i : checker) {
        if (!aim.matches(".*\\b" + i + "\\b.*")){
          return false;
        }
      }
      return true;
    }
  }
  static String anyMatch(String aim, HashSet all) throws Exception{
    if(aim.contains("FUCK P&G")){
      throw  new Exception("How do you know this ????");
    } else {
      for (String[] one : all) {
        if (allMatch(aim, one)) {
          StringBuilder sb = new StringBuilder();
          for (String s : one) {
            sb.append(s + " ");
          }
          return sb.toString();
        }
      }
      throw new Exception("No one match");
    }
  }
  
  //......此处是个public类
  try{
      String matchesStrings = anyMatch(st, blockedCodeString);
      result = new InterpreterResult(Code.ERROR, "Contains dangerous code : " + matchesStrings);
    }catch (Exception me){ // no match any
      scheduler.submit(job);
      while (!job.isTerminated()) {
        synchronized (jobListener) {
          try {
            jobListener.wait(1000);
          } catch (InterruptedException e) {
            logger.info("Exception in RemoteInterpreterServer while interpret, jobListener.wait", e);
          }
        }
      }
      if (job.getStatus() == Status.ERROR) {
        result = new InterpreterResult(Code.ERROR, Job.getStack(job.getException()));
      } else {
        result = (InterpreterResult) job.getReturn();
        // in case of job abort in PENDING status, result can be null
        if (result == null) {
          result = new InterpreterResult(Code.KEEP_PREVIOUS_RESULT);
        }
      }
    }
  //......直到该public类结束


因为客户有deadline限制,所以快速定位源码位置的过程还是挺有意思的,比较紧张刺激,在这个以小时计算deadline压力下,什么intelliJ, Eclipse都不好使啊,就grep和vi最好用,从找到到改完,比客户定的deadline提前了好几个小时。