본문 바로가기

SOFTWARE

PostgreSQL 사용법

PostgreSQL에서 확장의 SQL 함수로 암복호화합니다. 여러 행을 한꺼번에 처리할 때는 DB 밖에서 CLI로 일정 크기씩 나눠 처리합니다.

0.3.1갱신 2026-09-27MIT · Linux x86_64

시스템 요구 사항

PostgreSQL
18 (PostgreSQL 공식 PGDG 패키지)에서 시험했습니다. 명령의 경로와 서비스 이름은 PGDG 18 기준입니다.
DB 설정
데이터베이스 인코딩 UTF8
DB 서버 OS
Linux x86_64, glibc 2.28 이상 (예: RHEL·Rocky Linux 8 이상)
빌드 도구
gcc, make, redhat-rpm-config, postgresql18-devel(CRB 저장소 필요), curl, tar, sha256sum(coreutils 8.25 이상)
권한
DB 서버의 sudo 가능한 OS 계정, DB의 superuser(postgres)

PostgreSQL 18 외 버전, Debian과 Ubuntu 패키지, SELinux enforcing 구성은 시험하지 않았습니다.

실행 위치

단계실행 위치계정
1. 사전 점검 ~ 4. 서버에 키 경로 전달DB 서버 셸sudo 가능한 OS 계정
5. 확장 설치DB 서버 셸에서 sudo -iu postgres psqlpostgres(superuser), 애플리케이션 DB
6. 실행 권한 정리5단계와 같은 psqlpostgres, 같은 DB
7. 설치 확인psql 등 SQL 클라이언트암복호화를 쓸 애플리케이션 사용자, 같은 DB

PostgreSQL 백엔드 프로세스가 확장 라이브러리 pg_strcrypt.so를 불러오고, 확장이 같은 디렉토리의 libstrcrypt.so를 호출합니다. 라이브러리, 키 파일, 설정은 모두 DB 서버에 둡니다. 애플리케이션 서버에는 따로 설치할 것이 없습니다. 확장은 데이터베이스마다 따로 설치합니다. 명령의 appdb는 애플리케이션 DB 이름으로, APP_USER는 암복호화를 쓸 역할 이름으로 바꿉니다.

1. 사전 점검

ldd --version | head -1
systemctl is-active postgresql-18
sudo -iu postgres psql -Atc "SELECT version()" -c "SHOW server_encoding" -c "SELECT datname FROM pg_database WHERE datname = 'appdb'"
command -v gcc make curl sha256sum tar
rpm -q postgresql18-devel redhat-rpm-config

glibc 버전, 서비스 상태, PostgreSQL 버전, 데이터베이스 인코딩(UTF8), 대상 DB 이름이 차례로 나옵니다. command -v는 설치된 도구만 출력하고, 마지막 줄은 빌드 패키지가 없으면 is not installed를 출력합니다.

자주 발생하는 오류

증상원인과 조치
Unit postgresql.service not foundPGDG 18의 서비스 이름은 postgresql-18입니다.
server_encoding이 UTF8이 아님지원하지 않는 구성입니다. UTF8 데이터베이스에 설치합니다.
package postgresql18-devel is not installed2단계의 빌드 도구 설치 명령을 먼저 실행합니다.
ldd (GNU libc) 2.17 등 2.28 미만배포 파일을 쓸 수 없는 서버입니다.

2. 라이브러리 빌드와 설치

빌드 도구가 없을 때만

sudo dnf install -y --enablerepo=crb postgresql18-devel gcc make redhat-rpm-config

다운로드·검증·빌드·설치

PG_CONFIG=/usr/pgsql-18/bin/pg_config
mkdir -p ~/strcrypt-postgresql && cd ~/strcrypt-postgresql
curl -fLO https://www.cubiware.co.kr/api/download/strcrypt/0.3.1/libstrcrypt.so
curl -fLO https://www.cubiware.co.kr/api/download/strcrypt/0.3.1/libstrcrypt.so.hmac
curl -fLO https://www.cubiware.co.kr/api/download/strcrypt/0.3.1/strcrypt-postgresql-0.3.1.tar.gz
curl -fLO https://www.cubiware.co.kr/api/download/strcrypt/0.3.1/SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
tar xzf strcrypt-postgresql-0.3.1.tar.gz && cd strcrypt-postgresql-0.3.1
sudo install -m 755 ../libstrcrypt.so "$($PG_CONFIG --pkglibdir)/"
sudo install -m 644 ../libstrcrypt.so.hmac "$($PG_CONFIG --pkglibdir)/"
make PG_CONFIG="$PG_CONFIG" STRCRYPT_LIB_DIR="$($PG_CONFIG --pkglibdir)" STRCRYPT_INCLUDE_DIR=. with_llvm=no
sudo install -m 755 pg_strcrypt.so "$($PG_CONFIG --pkglibdir)/"
sudo install -m 644 pg_strcrypt.control pg_strcrypt--1.1.sql pg_strcrypt--1.0--1.1.sql "$($PG_CONFIG --sharedir)/extension/"
ldd "$($PG_CONFIG --pkglibdir)/pg_strcrypt.so" | grep strcrypt

확장은 DB 서버의 PostgreSQL 헤더로 빌드해야 하므로 소스로 제공합니다. 빌드는 ~/strcrypt-postgresql 아래에서 하고, 라이브러리 3개는 pg_config --pkglibdir(/usr/pgsql-18/lib)에, 확장 정의 파일 2개는 확장 디렉토리에 복사합니다. 마지막 줄이 libstrcrypt.so => /usr/pgsql-18/lib/libstrcrypt.so를 출력하면 성공입니다. 이 블록은 다시 실행할 수 있습니다.

자주 발생하는 오류

증상원인과 조치
nothing provides perl(IPC::Run) needed by postgresql18-develCRB 저장소가 꺼져 있습니다. --enablerepo=crb를 붙입니다. RHEL은 저장소 이름이 codeready-builder-for-rhel-9-x86_64-rpms입니다.
make: command not found빌드 도구 설치 명령을 먼저 실행합니다.
sha256sum 결과에 FAILED내려받는 중에 파일이 손상됐습니다. 그 파일을 다시 내려받습니다.
Error: libstrcrypt.so not found. / Run 'cargo build --release' in ... first.make 앞의 libstrcrypt.so 설치 줄을 빠뜨렸거나 STRCRYPT_LIB_DIR를 지정하지 않았습니다. 안내의 cargo 명령은 따르지 않습니다.
pgxs.mk: No such file or directorypostgresql18-devel이 설치되지 않았습니다.
cannot read spec file '/usr/lib/rpm/redhat/redhat-hardened-cc1'redhat-rpm-config가 설치되지 않았습니다.
make: /usr/bin/clang: No such file or directorymake 인자의 with_llvm=no를 빠뜨렸습니다.

3. 키 파일 생성

sudo install -d -o postgres -g postgres -m 700 /etc/strcrypt
sudo -u postgres sh -c 'umask 077 && set -C && head -c 24 /dev/urandom | base64 | head -c 32 > /etc/strcrypt/strcrypt.key'
sudo -u postgres stat -c '%a %U %s %n' /etc/strcrypt/strcrypt.key

32바이트 키를 postgres 계정만 읽을 수 있는 파일로 만듭니다. 마지막 줄이 600 postgres 32 /etc/strcrypt/strcrypt.key를 출력해야 합니다. 이미 키 파일이 있으면 덮어쓰지 않고 멈춥니다. 키는 SQL 인자나 테이블로 전달하지 않습니다. 키를 잃으면 암호문을 복구할 수 없으니 키를 따로 백업해 두세요.

자주 발생하는 오류

마지막 행을 뺀 키 파일 문제는 5단계 이후 SQL을 호출할 때 ERROR: strcrypt: ... 환경 변수 오류: 뒤에 붙어 나옵니다.

증상원인과 조치
권한 오류: 키 파일 권한은 0600 이어야 합니다chmod 600으로 고칩니다. 새로 접속한 세션부터 반영됩니다.
파일 오류 (/etc/strcrypt/strcrypt.key): Permission denied키 파일이나 /etc/strcrypt의 소유자가 postgres가 아닙니다.
잘못된 키 파일: 키 파일이 비어 있습니다빈 파일입니다. 파일을 지우고 두 번째 줄을 다시 실행합니다.
잘못된 키 파일: 키 파일 내용은 최대 32바이트여야 합니다32바이트를 넘는 값을 넣었습니다. 두 번째 줄의 명령으로 다시 만듭니다.
cannot overwrite existing file키 파일이 이미 있어 덮어쓰지 않았습니다. 기존 키로 만든 암호문이 있으면 그 키를 그대로 씁니다.

4. 서버에 키 경로 전달

sudo mkdir -p /etc/systemd/system/postgresql-18.service.d
printf '[Service]\nEnvironment=STRCRYPT_KEY_FILE=/etc/strcrypt/strcrypt.key\n' | sudo tee /etc/systemd/system/postgresql-18.service.d/strcrypt.conf
sudo systemctl daemon-reload
sudo systemctl restart postgresql-18
systemctl show -p Environment postgresql-18 | grep -o 'STRCRYPT_[A-Z_]*=[^ ]*'

systemd 설정 조각 파일로 PostgreSQL 서비스에 환경 변수 STRCRYPT_KEY_FILE을 넘깁니다. 마지막 줄이 STRCRYPT_KEY_FILE=/etc/strcrypt/strcrypt.key를 출력하면 반영된 것입니다. 재시작하는 동안 DB 접속이 잠시 끊깁니다. 셸에서 export한 값은 서버에 전달되지 않습니다. 국내 표준 외 알고리즘을 쓰려면 Environment=STRCRYPT_MODE=non-approved 줄을 추가합니다.

자주 발생하는 오류

증상원인과 조치
조회 시 STRCRYPT_KEY is required. create a key file ...키 경로가 서버에 전달되지 않았습니다. daemon-reload와 restart를 했는지 확인합니다. 메시지가 권하는 방법은 따르지 않습니다.

5. 확장 설치

sudo -iu postgres psql -d appdb -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS pg_strcrypt" -c "\dx pg_strcrypt" -c "SELECT strcrypt_decrypt(strcrypt_encrypt('test', 'aria256gcm'))"

appdb에 확장을 만들고 마지막 조회로 암복호화를 확인합니다. 출력 끝에 test가 나오면 성공입니다. 다시 실행하면 extension "pg_strcrypt" already exists, skipping이 더 나오고 결과는 같습니다.

자주 발생하는 오류

증상원인과 조치
ERROR: extension "pg_strcrypt" is not availablepg_strcrypt.control이 설치되지 않았습니다. 2단계를 확인합니다.
ERROR: could not access file "pg_strcrypt"pg_strcrypt.so가 설치되지 않았습니다.
ERROR: could not load library ... libstrcrypt.so: cannot open shared object filelibstrcrypt.so가 pg_strcrypt.so와 같은 디렉토리에 없습니다.
ERROR: ... 암호모듈이 에러 상태입니다: IntegrityFailurelibstrcrypt.so.hmac이 libstrcrypt.so와 짝이 맞지 않습니다. 같은 배포본에서 받은 두 파일을 함께 둡니다.
ERROR: permission denied to create extension "pg_strcrypt"superuser로 실행합니다. DB 소유자라도 superuser가 아니면 설치할 수 없습니다.
ERROR: invalid byte sequence for encoding "EUC_KR"UTF8이 아닌 데이터베이스입니다. 지원하지 않습니다.
끝의 조회에서 3, 4단계의 키 오류확장은 설치됐습니다. 3, 4단계를 고친 뒤 같은 명령을 다시 실행합니다.

6. 실행 권한 정리

BEGIN;
REVOKE EXECUTE ON FUNCTION strcrypt_encrypt(text), strcrypt_encrypt(text, text),
                           strcrypt_decrypt(text), strcrypt_decrypt(text, text),
                           strcrypt_search_index(text, text),
                           strcrypt_algorithms(), strcrypt_hw_info() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION strcrypt_encrypt(text), strcrypt_encrypt(text, text),
                          strcrypt_decrypt(text), strcrypt_decrypt(text, text),
                          strcrypt_search_index(text, text),
                          strcrypt_algorithms(), strcrypt_hw_info() TO APP_USER;
COMMIT;

확장 1.1은 암복호화 함수의 PUBLIC 실행 권한을 회수한 채 설치됩니다. 조회 함수까지 막고 애플리케이션 사용자에게만 권한을 주려면, sudo -iu postgres psql -d appdb -v ON_ERROR_STOP=1로 접속해 이 블록을 실행합니다. 출력은 BEGIN, REVOKE, GRANT, COMMIT입니다. 사용자가 여럿이면 TO 뒤에 쉼표로 잇습니다. 이 블록은 다시 실행할 수 있고, 확장을 지웠다가 다시 설치하면 다시 실행해야 합니다. superuser는 권한 검사를 받지 않고 SQL로 키 파일도 읽을 수 있으므로 superuser 계정을 애플리케이션에 쓰지 않습니다.

자주 발생하는 오류

증상원인과 조치
ERROR: role "..." does not existAPP_USER를 실제 역할 이름으로 바꿉니다. 트랜잭션으로 묶여 있어 REVOKE도 적용되지 않았습니다.
허용하지 않은 사용자의 ERROR: permission denied for function strcrypt_decrypt정상입니다. PUBLIC 권한이 회수됐습니다.
확장 버전이 1.0ALTER EXTENSION pg_strcrypt UPDATE;를 실행한 뒤 6단계를 다시 실행합니다.

7. 설치 확인

SHOW server_encoding;
SELECT strcrypt_decrypt(strcrypt_encrypt('test', 'aria256gcm'));
SELECT count(*) AS checked,
       count(*) FILTER (WHERE strcrypt_decrypt(strcrypt_encrypt(v, 'aria256gcm')) IS DISTINCT FROM v) AS mismatches
FROM (SELECT 'row-' || g || '-홍길동' AS v FROM generate_series(1, 1000) g) s;

애플리케이션 사용자로 같은 DB에 접속해 실행합니다(예: psql -h localhost -d appdb -U APP_USER). 차례로 UTF8, test, checked 1000 | mismatches 0이 나와야 합니다. 마지막 조회는 1,000행을 암호화했다가 복호화해 비교하며, 테이블을 만들지 않습니다.

자주 발생하는 오류

증상원인과 조치
ERROR: function strcrypt_encrypt(unknown, unknown) does not exist확장을 다른 데이터베이스에 설치했습니다. 애플리케이션이 접속하는 DB에서 5, 6단계를 실행합니다.
ERROR: permission denied for function strcrypt_decrypt6단계의 권한 부여 대상이 이 사용자인지 확인합니다.

SQL 함수

CREATE TABLE customers (id bigint PRIMARY KEY, name text, ssn text, ssn_enc text);
INSERT INTO customers (id, name, ssn_enc)
VALUES (1, '홍길동', strcrypt_encrypt('900101-1234567', 'aria256gcm'));
SELECT id, name, strcrypt_decrypt(ssn_enc) AS ssn FROM customers;
SELECT strcrypt_encrypt('alice@example.com', 'seed128gcm');

SELECT, INSERT, UPDATE 어디서나 일반 함수처럼 씁니다. 입력이 NULL이면 결과도 NULL입니다. 함수는 모두 병렬 쿼리에서 쓸 수 있습니다. 암호화할 때는 알고리즘을 항상 지정합니다. 알고리즘을 뺀 strcrypt_encrypt(input)의 암호문은 헤더가 없어, 서버의 동작 모드(STRCRYPT_MODE)를 바꾸면 복호화되지 않습니다.

암호문은 같은 값이어도 매번 달라서 인덱스로 찾을 수 없습니다. 같은 값 검색이 필요하면 strcrypt_search_index(값, '테이블.열') 결과를 검색용 컬럼에 저장하고 인덱스를 겁니다. 조회할 때도 같은 함수로 검색어의 인덱스를 만들어 비교합니다. 키 없는 SHA-256 해시는 쓰지 않습니다. 주민번호처럼 추측할 수 있는 값은 후보를 모두 해시해 보면 원래 값이 나옵니다. 검색용 컬럼은 같은 값을 가진 행이 어느 것인지 드러내므로 필요한 컬럼에만 두고, 검색 인덱스 함수 실행 권한은 복호화 권한처럼 필요한 사용자에게만 줍니다.

대량 처리

PostgreSQL에는 여러 행을 한 번에 받는 SQL 함수가 없습니다. 대량 UPDATE의 시간은 대부분 테이블과 인덱스 쓰기가 차지해 SQL 안에서 묶어도 빨라지지 않기 때문입니다. 여러 행은 DB 밖에서 기본 키 순서로 청크를 조회해 CLI로 한 번에 암복호화하고, 기본 키 조건으로 반영합니다. CLI로 만든 암호문은 SQL 함수로, SQL 함수로 만든 암호문은 CLI로 복호화할 수 있습니다.

CLI 준비

mkdir -p ~/strcrypt && cd ~/strcrypt
curl -fLO https://www.cubiware.co.kr/api/download/strcrypt/0.3.1/strcrypt
curl -fLO https://www.cubiware.co.kr/api/download/strcrypt/0.3.1/strcrypt.hmac
curl -fLO https://www.cubiware.co.kr/api/download/strcrypt/0.3.1/SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
chmod +x strcrypt
sudo install -o "$USER" -m 600 /etc/strcrypt/strcrypt.key ~/strcrypt/strcrypt.key

CLI는 DB와 같은 키를 써야 합니다. 마지막 줄은 DB 서버에서 CLI를 실행할 때 현재 계정이 읽을 키 사본을 만듭니다. 작업이 끝나면 사본을 지웁니다.

기존 컬럼 일괄 암호화

cd ~/strcrypt
export STRCRYPT_KEY_FILE="$HOME/strcrypt/strcrypt.key"
export PGHOST=localhost PGDATABASE=appdb PGUSER=APP_USER
set -o pipefail
umask 077
last=0
while :; do
  psql -X -At -F $'\t' -v ON_ERROR_STOP=1 -c "
    SELECT id, translate(encode(convert_to(ssn, 'UTF8'), 'base64'), E'\n', '')
      FROM customers
     WHERE id > $last AND ssn IS NOT NULL AND ssn_enc IS NULL
     ORDER BY id LIMIT 1000" > chunk.tsv || break
  [ -s chunk.tsv ] || break
  cut -f2 chunk.tsv | ./strcrypt encrypt-batch -a aria256gcm -o chunk.enc || break
  paste <(cut -f1 chunk.tsv) chunk.enc > chunk.load
  psql -X -q -v ON_ERROR_STOP=1 <<'SQL' || break
BEGIN;
CREATE TEMP TABLE enc_chunk (id bigint PRIMARY KEY, ssn_enc text) ON COMMIT DROP;
\copy enc_chunk FROM 'chunk.load'
UPDATE customers c SET ssn_enc = e.ssn_enc FROM enc_chunk e WHERE c.id = e.id;
COMMIT;
SQL
  last=$(tail -n 1 chunk.tsv | cut -f1)
  echo "encrypted up to id $last"
done
rm -f chunk.tsv chunk.enc chunk.load

1,000행씩 평문을 Base64로 조회해 encrypt-batch로 암호화하고, 임시 테이블에 적재해 기본 키로 반영한 뒤 청크마다 커밋합니다. 청크마다 encrypted up to id ...를 출력합니다. ssn_enc IS NULL 조건이 있어 중간에 멈추면 같은 명령으로 이어서 처리합니다. 비밀번호는 ~/.pgpass나 PGPASSWORD로 줍니다.

  • encode(..., 'base64')는 76자마다 줄바꿈을 넣습니다. translate로 줄바꿈을 지우지 않으면 한 행이 여러 레코드로 나뉘어 행이 어긋나고, CLI는 오류를 내지 않습니다.
  • 빈 줄은 빈 문자열로 암호화되므로 ssn IS NOT NULL 조건을 뺄 수 없습니다.
  • 청크 파일은 평문 Base64를 담으므로 umask 077로 현재 계정만 읽게 만들고 끝에 지웁니다.

결과 대조 (APP_USER로 실행)

SELECT count(*) AS total,
       count(ssn_enc) AS encrypted,
       count(*) FILTER (WHERE ssn IS NOT NULL AND strcrypt_decrypt(ssn_enc) IS DISTINCT FROM ssn) AS mismatches
FROM customers;

mismatches가 0이면 CLI가 만든 암호문이 모두 SQL 함수로 복호화된 것입니다.

SQL 함수가 만든 암호문을 CLI로 복호화

cd ~/strcrypt
export STRCRYPT_KEY_FILE="$HOME/strcrypt/strcrypt.key"
export PGHOST=localhost PGDATABASE=appdb PGUSER=APP_USER
set -o pipefail
umask 077
psql -X -At -F $'\t' -v ON_ERROR_STOP=1 -c "SELECT id, ssn_enc FROM customers WHERE ssn_enc IS NOT NULL ORDER BY id" > enc.tsv
cut -f2 enc.tsv | ./strcrypt decrypt-batch | paste <(cut -f1 enc.tsv) - > dec.load
psql -X -v ON_ERROR_STOP=1 <<'SQL'
CREATE TEMP TABLE dec_rows (id bigint PRIMARY KEY, ssn_b64 text);
\copy dec_rows FROM 'dec.load'
SELECT count(*) AS checked,
       count(*) FILTER (WHERE convert_from(decode(d.ssn_b64, 'base64'), 'UTF8') IS DISTINCT FROM c.ssn) AS mismatches
FROM dec_rows d JOIN customers c USING (id);
SQL
rm -f enc.tsv dec.load

decrypt-batch는 평문을 Base64로 내보내므로 SQL에서 decode로 되돌려 비교합니다. checked는 암호문이 있는 행 수, mismatches는 0이어야 합니다.

제한 사항

  • UTF8 데이터베이스만 지원합니다. EUC_KR 데이터베이스에는 확장이 설치되지 않고, LATIN1 데이터베이스에서는 비 ASCII 값이 오류로 끝납니다.
  • 결과 Base64가 PostgreSQL 값 크기 한도(1GB) 안에 들어가야 합니다. 평문 약 800MB가 상한입니다.
  • 한 문장에서 한 행이라도 실패하면 그 문장 전체가 오류로 끝납니다.
  • 같은 값도 암호화할 때마다 암호문이 달라지므로 암호문 컬럼으로는 검색하거나 인덱스를 만들 수 없습니다. 검색이 필요하면 해시 컬럼을 따로 둡니다.
  • 평문은 DB 서버의 백엔드 프로세스 메모리를 거칩니다. SQL 문에 평문을 직접 쓰면 서버 로그 설정에 따라 로그에 남을 수 있습니다.
  • 키 파일의 내용이나 권한을 바꾸면 새로 접속한 세션부터, 환경 변수를 바꾸면 서버를 다시 시작한 뒤부터 반영됩니다.

API 레퍼런스

함수설명
strcrypt_encrypt(input text, algorithm text)지정한 알고리즘으로 암호화해 Base64 문자열을 돌려줌. 새 데이터에 권장
strcrypt_decrypt(encrypted_text text)복호화. 암호문 앞의 헤더로 알고리즘을 자동으로 인식
strcrypt_decrypt(encrypted_text text, algorithm text)알고리즘을 지정해 복호화. algorithm이 NULL이면 자동 인식
strcrypt_encrypt(input text)알고리즘 헤더 없는 호환 형식으로 암호화. 서버 동작 모드가 알고리즘을 정함
strcrypt_search_index(value text, domain text)같은 값 검색용 인덱스(16진 64자). domain은 열을 구분하는 문자열(예: customers.ssn). Python·Node.js·Java 바인딩과 같은 값
strcrypt_algorithms() / strcrypt_hw_info()알고리즘 이름 목록, 하드웨어 가속 정보(JSON 문자열)

모든 함수는 text를 돌려줍니다. 기본 모드에서 허용하지 않는 알고리즘을 지정하면 현재 모드(korean-standard)에서 허용되지 않는 알고리즘 오류가 납니다.

시험 환경

환경방법
Rocky Linux 9.8 (glibc 2.34), PostgreSQL 18 PGDG 패키지, systemd이 페이지의 배포 파일과 명령을 그대로 실행

1~7단계, SQL 함수 예시, 대량 처리 예시(2,345행 왕복, NULL, 빈 문자열, 줄바꿈, 1KB 한글 값 포함), 권한 회수 후 허용하지 않은 사용자의 호출 거부를 확인했습니다(2026-09-17).

이미지를 클릭하면 원본 크기 · 바깥 클릭이나 Esc로 닫기