키링(Keyring) (Key Retention Service)
Linux 커널의 Key Retention Service는 암호화(Encryption) 키, 인증 토큰, 기타 보안 데이터를 커널 공간(Kernel Space)에 안전하게 저장하고 관리하는 프레임워크입니다. struct key 기반의 자료구조, 다양한 key_type(user, logon, encrypted, trusted, big_key, asymmetric), 프로세스별 키링 계층, keyctl 시스템 콜(System Call), request_key upcall 메커니즘, TPM Sealed Keys, X.509/PKCS#7 인증서 검증, fscrypt/dm-crypt 키 연동, watch_queue 알림까지 커널 소스 기반으로 심층 분석합니다.
핵심 요약
- Key Retention Service — 암호화 키와 인증 토큰을 커널 공간에 캐싱해 사용자 공간 프로세스와 커널 서비스가 안전하게 공유하도록 합니다.
struct key— 모든 키의 기본 자료구조로 참조 카운트, 만료 시간, 접근 권한, 페이로드(Payload)를 담습니다.- key_type 시스템 — user, logon, encrypted, trusted, big_key, asymmetric 등 여러 유형을 플러그인 방식으로 지원합니다.
- 키링 계층 — session/process/thread와 user/user_session 계층이 키의 범위·수명을 결정합니다.
- 시스템 콜 3종 —
add_key(),request_key(),keyctl()이 키 등록·조회·조작의 유일한 진입점입니다.
단계별 이해
- 키와 키링 구분
개별 키와 그것을 담는 키링 컨테이너(Container)를 먼저 구분합니다. - 자료구조 확인
struct key,key_type, 권한 모델이 어떻게 연결되는지 봅니다. - 시스템 콜 흐름 이해
add_key(),request_key(),keyctl()가 어떤 상황에서 쓰이는지 정리합니다. - 실사용 시나리오 연결
fscrypt, dm-crypt, 커널 모듈 서명처럼 실제 서브시스템이 키링을 어떻게 활용하는지 확인합니다. - 수명주기와 보안 검토
만료, quota, request-key upcall, 플랫폼 키링까지 운영 관점에서 정리합니다.
Key Retention Service 아키텍처
Key Retention Service는 Linux 2.6.10에서 David Howells에 의해 도입되었습니다. 핵심 목적은 암호화 키와 인증 토큰을 커널 공간에 안전하게 보관하고, 프로세스 간 공유를 제어하며, 키의 생명주기를 자동 관리하는 것입니다. 소스 코드는 security/keys/ 디렉토리에 위치합니다.
키링 서브시스템의 주요 구성 요소는 다음과 같습니다.
| 구성 요소 | 소스 위치 | 역할 |
|---|---|---|
| key.c | security/keys/key.c | struct key 생성, 검색, 삭제, 참조 카운트 관리 |
| keyring.c | security/keys/keyring.c | 키링(키 컬렉션) 관리, 연관 배열 기반 검색 |
| keyctl.c | security/keys/keyctl.c | keyctl() 시스템 콜 구현 |
| request_key.c | security/keys/request_key.c | request_key upcall 메커니즘 |
| gc.c | security/keys/gc.c | 만료/폐기 키의 가비지 컬렉션 |
| permission.c | security/keys/permission.c | 키 접근 권한 검사 |
| encrypted-keys/ | security/keys/encrypted-keys/ | encrypted 키 타입 구현 |
| trusted-keys/ | security/keys/trusted-keys/ | TPM sealed trusted 키 타입 구현 |
| big_key.c | security/keys/big_key.c | shmem 기반 대용량 키 저장 |
struct key 자료구조
struct key(include/linux/key.h)는 키링 서비스의 핵심 자료구조입니다. 모든 키(키링 포함)는 이 구조체(Struct)의 인스턴스로 표현됩니다.
/* include/linux/key.h */
struct key {
refcount_t usage; /* 참조 카운트 */
key_serial_t serial; /* 키 일련번호 (고유 ID) */
union {
struct list_head graveyard_link;
struct rb_node serial_node;
};
struct rw_semaphore sem; /* 키 내용 접근 세마포어 */
struct key_user *user; /* 소유자 사용자 정보 */
void *security; /* LSM 보안 레이블 */
union {
time64_t expiry; /* 만료 시각 (0 = 무기한) */
time64_t revoked_at; /* 폐기 시각 */
};
time64_t last_used_at; /* 마지막 사용 시각 */
kuid_t uid; /* 소유자 UID */
kgid_t gid; /* 소유자 GID */
key_perm_t perm; /* 접근 권한 비트마스크 */
unsigned short quotalen; /* 할당량에 반영되는 페이로드 길이 */
unsigned short datalen; /* 페이로드 실제 길이 */
short state; /* 키 상태 */
unsigned long flags; /* KEY_FLAG_* 플래그 */
union {
struct keyring_index_key index_key; /* 검색 인덱스 키 */
struct {
unsigned long hash;
unsigned long len_desc;
struct key_type *type; /* 키 타입 */
struct key_tag *domain_tag;
char *description; /* 키 설명(이름) */
};
};
union {
union key_payload payload; /* 키 페이로드 (실제 데이터) */
struct {
struct list_head name_link;
struct assoc_array keys; /* 키링의 경우: 하위 키 목록 */
};
};
};
주요 필드를 분석합니다.
| 필드 | 타입 | 설명 |
|---|---|---|
usage | refcount_t | 참조 카운트. key_get()/key_put()으로 증감. 0이 되면 GC 대상 |
serial | key_serial_t | 커널 전역 고유 일련번호. 사용자 공간에서 키를 식별하는 핸들 |
sem | rw_semaphore | 키 페이로드 읽기/쓰기 동기화. read는 키 사용, write는 키 갱신 |
perm | key_perm_t | 30비트 접근 권한. possessor/user/group/other 각 7비트 + 2비트 예약 |
flags | unsigned long | KEY_FLAG_DEAD, KEY_FLAG_REVOKED, KEY_FLAG_IN_QUOTA 등 상태 플래그 |
state | short | 양수: 인스턴스화 완료, 0: 대기 중, 음수: 오류 코드 |
payload | union key_payload | 실제 키 데이터. key_type의 콜백(Callback) 함수가 해석 |
type | struct key_type * | 이 키의 타입. 인스턴스화, 검증, 비교, 파괴 등의 콜백 제공 |
키 일련번호(serial): 양의 정수로 자동 할당되며, 사용자 공간에서 키를 참조할 때 사용합니다. 특수 일련번호가 있습니다: KEY_SPEC_THREAD_KEYRING(-1), KEY_SPEC_PROCESS_KEYRING(-2), KEY_SPEC_SESSION_KEYRING(-3), KEY_SPEC_USER_KEYRING(-4), KEY_SPEC_USER_SESSION_KEYRING(-5).
key_type 시스템
struct key_type은 키의 유형을 정의하는 플러그인 인터페이스입니다. 각 key_type은 키 페이로드의 인스턴스화, 검증, 비교, 읽기, 파괴 등의 콜백 함수를 제공합니다.
struct key_type의 주요 콜백 함수:
/* include/linux/key-type.h — 간략화 */
struct key_type {
const char *name; /* 타입 이름 ("user", "logon" 등) */
size_t def_datalen; /* 기본 페이로드 크기 */
unsigned flags; /* KEY_TYPE_* 플래그 */
/* 키 인스턴스화 — 페이로드 데이터를 키에 저장 */
int (*preparse)(struct key_preparsed_payload *prep);
void (*free_preparse)(struct key_preparsed_payload *prep);
int (*instantiate)(struct key *key,
struct key_preparsed_payload *prep);
/* 키 갱신 */
int (*update)(struct key *key,
struct key_preparsed_payload *prep);
/* 키 매칭 — 검색 시 비교 함수 */
int (*match_preparse)(struct key_match_data *match_data);
void (*match_free)(struct key_match_data *match_data);
/* 키 폐기/파괴 */
void (*revoke)(struct key *key);
void (*destroy)(struct key *key);
/* 사용자 공간으로 키 내용 읽기 */
long (*read)(const struct key *key,
char *buffer, size_t buflen);
/* /proc/keys 표시 */
void (*describe)(const struct key *key,
struct seq_file *m);
};
새로운 키 타입을 등록하려면 register_key_type()을, 해제하려면 unregister_key_type()을 호출합니다. 커널 모듈에서 사용자 정의 키 타입을 추가할 수 있습니다.
user / logon 키 타입
user 타입은 가장 기본적인 키 타입으로, 임의의 바이트 블롭(blob)을 저장합니다. 사용자 공간에서 KEYCTL_READ로 내용을 읽을 수 있습니다.
/* security/keys/user_defined.c */
struct key_type key_type_user = {
.name = "user",
.def_datalen = 0,
.preparse = user_preparse,
.free_preparse = user_free_preparse,
.instantiate = generic_key_instantiate,
.update = user_update,
.revoke = user_revoke,
.destroy = user_destroy,
.describe = user_describe,
.read = user_read, /* 사용자 공간 read 허용 */
};
# user 타입 키 생성 및 읽기
$ keyctl add user mykey "hello world" @s
123456789
$ keyctl print 123456789
hello world
$ keyctl pipe 123456789 | hexdump -C
00000000 68 65 6c 6c 6f 20 77 6f 72 6c 64 |hello world|
logon 타입은 user 타입과 동일한 저장 방식을 사용하지만, read 콜백이 없어 사용자 공간에서 키 내용을 읽을 수 없습니다. 커널만 접근 가능하므로 dm-crypt, fscrypt, ecryptfs 등에서 사용합니다.
/* security/keys/user_defined.c — 간략화 */
struct key_type key_type_logon = {
.name = "logon",
.def_datalen = 0,
.preparse = user_preparse,
.free_preparse = user_free_preparse,
.instantiate = generic_key_instantiate,
.update = user_update,
.revoke = user_revoke,
.destroy = user_destroy,
.describe = user_describe,
/* .read 콜백 없음 → 사용자 공간에서 read 불가 */
};
| 비교 항목 | user 타입 | logon 타입 |
|---|---|---|
| 사용자 공간 read | 허용 (KEYCTL_READ) | 차단 (-EOPNOTSUPP) |
| 사용자 공간 write | 허용 (add_key()) | 허용 (add_key()) |
| 커널 접근 | 허용 | 허용 |
| 주요 용도 | 범용 시크릿 저장 | dm-crypt, fscrypt 키 |
| description 접두사 | 제한 없음 | 서비스명:설명 형식 필수 |
logon 키의 description 규칙: logon 타입 키는 반드시 서비스명:설명 형식이어야 합니다(예: dm-crypt:my-volume). 콜론이 없으면 -EINVAL을 반환합니다. 이는 서로 다른 서비스 간 키 이름 충돌을 방지합니다.
encrypted 키 타입
encrypted 키는 마스터 키(user 또는 trusted 타입)로 암호화되어 저장되는 키입니다. 마스터 키 없이는 평문을 복원할 수 없으므로, 디스크에 안전하게 저장할 수 있습니다. 암호화 알고리즘은 AES-256-GCM을 사용합니다.
# 1. 마스터 키 생성 (user 타입)
$ keyctl add user master-key "$(dd if=/dev/urandom bs=32 count=1 2>/dev/null)" @s
456789012
# 2. encrypted 키 생성 (마스터 키로 보호)
$ keyctl add encrypted my-enc-key "new user:master-key 64" @s
567890123
# 3. encrypted 키의 암호문 읽기 (평문이 아닌 암호화된 blob)
$ keyctl pipe 567890123 | hexdump -C | head -5
# 4. 디스크에 저장 후 나중에 복원
$ keyctl pipe 567890123 > /etc/keys/my-enc-key.blob
$ keyctl add encrypted my-enc-key "load $(cat /etc/keys/my-enc-key.blob)" @s
encrypted 키의 구조:
/* security/keys/encrypted-keys/encrypted.c */
struct encrypted_key_payload {
struct rcu_head rcu;
char *format; /* "default" 또는 "ecryptfs" */
char *master_desc; /* 마스터 키 description */
char *datalen; /* 평문 데이터 길이 */
u8 *iv; /* 초기화 벡터 */
u8 *encrypted_data; /* 암호화된 데이터 */
unsigned short payload_datalen; /* 전체 페이로드 길이 */
u8 decrypted_data[]; /* 복호화된 평문 (커널 메모리) */
};
encrypted 키의 내부 암호화 과정을 상세히 분석합니다.
/* security/keys/encrypted-keys/encrypted.c — 암호화 핵심 */
static int encrypted_key_encrypt(
struct encrypted_key_payload *epayload,
const struct key *mkey)
{
struct crypto_aead *tfm;
struct aead_request *req;
struct scatterlist sg[2];
const struct user_key_payload *ukp;
/* 1. 마스터 키 페이로드 가져오기 */
ukp = user_key_payload_locked(mkey);
if (!ukp || ukp->datalen < MIN_KEY_SIZE)
return -EINVAL;
/* 2. AES-256-GCM 트랜스폼 할당 */
tfm = crypto_alloc_aead("gcm(aes)", 0, CRYPTO_ALG_ASYNC);
crypto_aead_setkey(tfm, ukp->data, ukp->datalen);
crypto_aead_setauthsize(tfm, GCM_AES_IV_SIZE); /* 16바이트 태그 */
/* 3. IV 생성 (커널 RNG) */
get_random_bytes(epayload->iv, GCM_AES_IV_SIZE);
/* 4. 평문 → 암호문 변환 */
sg_init_table(sg, 2);
sg_set_buf(&sg[0], epayload->decrypted_data,
epayload->payload_datalen);
sg_set_buf(&sg[1], epayload->encrypted_data,
epayload->payload_datalen + GCM_AES_IV_SIZE);
aead_request_set_crypt(req, sg, sg,
epayload->payload_datalen,
epayload->iv);
return crypto_aead_encrypt(req);
}
마스터 키 계층화: encrypted 키의 마스터 키를 trusted 타입으로 사용하면, TPM 하드웨어까지 이어지는 키 보호 체인을 구성할 수 있습니다. encrypted(trusted:tpm-master) → dm-crypt 볼륨 키 형태가 가장 안전한 구성입니다.
trusted 키 타입 (TPM Sealed Keys)
trusted 키는 TPM(Trusted Platform Module) 하드웨어에 의해 봉인(seal)되는 키입니다. TPM의 SRK(Storage Root Key)로 암호화되어 저장되며, 특정 PCR(Platform Configuration Register) 값과 바인딩할 수 있습니다. TPM 없이는 키를 복원할 수 없으므로 최고 수준의 키 보호를 제공합니다.
trusted 키의 핵심 구조체:
/* security/keys/trusted-keys/trusted_core.c */
struct trusted_key_payload {
struct rcu_head rcu;
unsigned int key_len; /* 평문 키 길이 */
unsigned int blob_len; /* 봉인 blob 길이 */
unsigned char migratable; /* 마이그레이션 허용 여부 */
unsigned char old_format; /* 이전 형식 호환 */
unsigned char key[MAX_KEY_SIZE + 1]; /* 평문 키 데이터 */
unsigned char blob[MAX_BLOB_SIZE]; /* TPM 봉인 blob */
};
trusted 키 소스: Linux 5.13부터 TPM 외에 ARM TrustZone TEE(OP-TEE)와 CAAM(NXP Cryptographic Acceleration and Assurance Module)도 trusted 키의 백엔드로 사용할 수 있습니다. CONFIG_TRUSTED_KEYS_TPM, CONFIG_TRUSTED_KEYS_TEE, CONFIG_TRUSTED_KEYS_CAAM으로 선택합니다.
TPM Sealed Keys: seal/unseal 흐름 다이어그램, EA 정책(PolicyPCR/PolicyPassword) 봉인, keyctl 실습 예제, tpm2-tools 실전 레시피 등은 TPM 2.0 문서를 참조하세요.
big_key 키 타입
big_key 타입은 대용량 키 데이터(최대 1MiB)를 효율적으로 저장합니다. 작은 키(기본적으로 PAGE_SIZE 이하)는 일반 kmalloc 메모리에 저장하지만, 큰 키는 shmem/tmpfs 파일에 AES 암호화하여 저장합니다. 이로써 커널 직접 메모리 사용을 최소화하면서도 대용량 인증서 체인이나 Kerberos 티켓을 저장할 수 있습니다.
/* security/keys/big_key.c */
enum {
big_key_data, /* 작은 키: kmalloc 직접 저장 */
big_key_path, /* 큰 키: shmem 파일 경로 */
};
struct key_type key_type_big_key = {
.name = "big_key",
.preparse = big_key_preparse,
.free_preparse = big_key_free_preparse,
.instantiate = generic_key_instantiate,
.revoke = big_key_revoke,
.destroy = big_key_destroy,
.describe = big_key_describe,
.read = big_key_read,
.def_datalen = 0,
};
/* 큰 키 저장 시 AES 암호화 */
static int big_key_preparse(struct key_preparsed_payload *prep)
{
if (prep->datalen > BIG_KEY_FILE_THRESHOLD) {
/* shmem 파일 생성 + AES-GCM 암호화 후 저장 */
big_key_crypt(BIG_KEY_ENC, ...);
} else {
/* 일반 kmalloc으로 직접 저장 */
prep->payload.data[big_key_data] = kmemdup(...);
}
}
| 키 크기 | 저장 방식 | 암호화 | 메모리 영향 |
|---|---|---|---|
| ≤ PAGE_SIZE | kmalloc (직접 메모리) | 없음 | 커널 직접 매핑(Mapping) |
| > PAGE_SIZE ~ 1 MiB | shmem/tmpfs 파일 | AES-GCM (커널 RNG IV) | 페이지 캐시(Page Cache) (스왑(Swap) 가능) |
asymmetric 키 타입 (X.509 / PKCS#7)
asymmetric 키 타입은 X.509 인증서, PKCS#7 서명, 공개키/개인키 쌍을 저장합니다. 커널 모듈 서명 검증, IMA/EVM 서명 검증, kexec 서명 검증 등에서 사용됩니다.
/* crypto/asymmetric_keys/asymmetric_type.c */
struct key_type key_type_asymmetric = {
.name = "asymmetric",
.preparse = asymmetric_key_preparse,
.instantiate = generic_key_instantiate,
.match_preparse = asymmetric_key_match_preparse,
.destroy = asymmetric_key_destroy,
.describe = asymmetric_key_describe,
.lookup_restriction = asymmetric_lookup_restriction,
};
/* 비대칭 키 하위 타입 */
struct asymmetric_key_subtype {
const char *name;
int (*verify_signature)(const struct key *key,
const struct public_key_signature *sig);
int (*query)(const struct kernel_pkey_params *params,
struct kernel_pkey_query *info);
};
지원되는 비대칭 키 알고리즘:
| 알고리즘 | 커널 설정 | 용도 |
|---|---|---|
| RSA | CONFIG_CRYPTO_RSA | 모듈 서명, X.509, PKCS#7 |
| ECDSA (P-256, P-384) | CONFIG_CRYPTO_ECDSA | IMA 서명, 경량 인증서 |
| EdDSA (Ed25519) | CONFIG_CRYPTO_ECRDSA | GOST 표준 서명 |
| SM2 | CONFIG_CRYPTO_SM2 | 중국 국가 표준 (GB/T 32918) |
키링 계층 구조
Linux는 프로세스별, 사용자별로 키링을 계층적으로 관리합니다. 키를 검색할 때 thread → process → session 순으로 탐색하며, 각 키링은 struct key의 특수 인스턴스입니다(key_type은 "keyring").
| 키링 | 특수 ID | 수명 | 공유 범위 | 생성 시점 |
|---|---|---|---|---|
| Thread | KEY_SPEC_THREAD_KEYRING (-1) | 스레드(Thread) 종료 시 파괴 | 해당 스레드만 | 첫 접근 시 지연(Latency) 생성 |
| Process | KEY_SPEC_PROCESS_KEYRING (-2) | 프로세스 종료 시 파괴 | 같은 TGID의 모든 스레드 | 첫 접근 시 지연 생성 |
| Session | KEY_SPEC_SESSION_KEYRING (-3) | 세션 종료 시 파괴 | 세션의 모든 프로세스 | PAM 또는 keyctl session |
| User | KEY_SPEC_USER_KEYRING (-4) | UID 네임스페이스(Namespace) 수명 | 같은 UID의 모든 프로세스 | UID 첫 사용 시 |
| User Session | KEY_SPEC_USER_SESSION_KEYRING (-5) | UID 네임스페이스 수명 | 같은 UID의 모든 프로세스 | UID 첫 사용 시 |
# 현재 프로세스의 키링 확인
$ keyctl show
Session Keyring
123456789 --alswrv 1000 1000 keyring: _ses
234567890 --alswrv 1000 1000 \_ keyring: _uid.1000
345678901 --alswrv 1000 1000 \_ user: my-secret
# 새 세션 키링 생성
$ keyctl session my-session
Joined session keyring: 456789012
# 키링 재귀 검색
$ keyctl search @s user my-secret
keyctl 시스템 콜 / API
키링 서비스는 3개의 시스템 콜을 통해 사용자 공간과 상호작용합니다.
/* include/uapi/linux/keyctl.h */
/* 1. 키 생성/추가 */
key_serial_t add_key(
const char *type, /* 키 타입 ("user", "logon", ...) */
const char *description, /* 키 설명/이름 */
const void *payload, /* 키 데이터 */
size_t plen, /* 데이터 길이 */
key_serial_t ringid /* 대상 키링 (@s, @u 등) */
);
/* 2. 키 검색/요청 */
key_serial_t request_key(
const char *type, /* 키 타입 */
const char *description, /* 키 설명 */
const char *callout_info, /* upcall 매개변수 (없으면 NULL) */
key_serial_t dest_keyring /* 결과 키를 저장할 키링 */
);
/* 3. 키 제어 (다목적) */
long keyctl(
int operation, /* KEYCTL_* 명령 */
... /* 명령별 가변 인자 */
);
주요 KEYCTL_* 명령:
| 명령 | 값 | 설명 |
|---|---|---|
KEYCTL_GET_KEYRING_ID | 0 | 특수 키링 ID를 실제 serial로 변환 |
KEYCTL_JOIN_SESSION_KEYRING | 1 | 새 세션 키링 생성 또는 기존 키링에 조인 |
KEYCTL_UPDATE | 2 | 키 페이로드 갱신 |
KEYCTL_REVOKE | 3 | 키 폐기 (사용 불가 처리) |
KEYCTL_DESCRIBE | 6 | 키 메타데이터 조회 (타입, UID, GID, perm) |
KEYCTL_CLEAR | 7 | 키링의 모든 키 제거 |
KEYCTL_LINK | 8 | 키를 키링에 링크 |
KEYCTL_UNLINK | 9 | 키를 키링에서 언링크 |
KEYCTL_SEARCH | 10 | 키링 재귀 검색 |
KEYCTL_READ | 11 | 키 페이로드 읽기 |
KEYCTL_SET_TIMEOUT | 15 | 키 만료 시간 설정 (초 단위) |
KEYCTL_SET_PERM | 5 | 키 접근 권한 설정 |
KEYCTL_INVALIDATE | 21 | 키 즉시 무효화(Invalidation) (GC 대기 없이) |
KEYCTL_RESTRICT_KEYRING | 29 | 키링에 추가 가능한 키 제한 |
KEYCTL_WATCH_KEY | 32 | 키 변경 이벤트 감시 (watch_queue) |
커널 내부 API:
/* include/linux/key.h — 커널 내부 키 API 간략화 */
struct key *request_key(struct key_type *type,
const char *description,
const char *callout_info);
/* 키 페이로드에 접근 (RCU 보호) */
const void *user_key_payload_locked(const struct key *key);
/* 키 참조 카운트 관리 */
struct key *key_get(struct key *key);
void key_put(struct key *key);
/* 키 인스턴스화 (request_key upcall 콜백에서 사용) */
int key_instantiate_and_link(struct key *key,
const void *data, size_t datalen,
struct key *keyring,
struct key *authkey);
keyctl 유저스페이스 도구
keyutils 패키지는 키링 서비스의 사용자 공간 인터페이스를 제공합니다. keyctl 명령어와 libkeyutils 라이브러리로 구성됩니다.
# 패키지 설치
$ apt install keyutils # Debian/Ubuntu
$ dnf install keyutils # Fedora/RHEL
# 기본 키 관리
$ keyctl add user mykey "secret data" @s # 키 추가
$ keyctl update 123456 "new data" # 키 갱신
$ keyctl print 123456 # 키 읽기 (텍스트)
$ keyctl pipe 123456 > /tmp/key.bin # 키 읽기 (바이너리)
$ keyctl revoke 123456 # 키 폐기
$ keyctl unlink 123456 @s # 키링에서 제거
# 키링 관리
$ keyctl show # 현재 키링 트리 표시
$ keyctl show @s # 세션 키링만 표시
$ keyctl newring mykeyring @s # 서브 키링 생성
$ keyctl link 123456 %:mykeyring # 키를 서브 키링에 링크
# 키 메타데이터 조회
$ keyctl describe 123456
user;1000;1000;3f010000;mykey
# 접근 권한 변경 (possessor=all, user=view+read, group=none, other=none)
$ keyctl setperm 123456 0x3f030000
# 만료 시간 설정 (3600초 = 1시간)
$ keyctl timeout 123456 3600
# 세션 키링 전환
$ keyctl session newsession /bin/bash # 새 세션으로 bash 실행
# /proc/keys: 현재 프로세스가 볼 수 있는 모든 키
$ cat /proc/keys
00000001 I--Q--- 1 perm 1f3f0000 0 0 keyring _uid_ses.0: 1
00000002 I--Q--- 2 perm 1f3f0000 0 0 keyring _uid.0: empty
# /proc/key-users: 사용자별 키 사용 통계
$ cat /proc/key-users
0: 4 3/3 2/200 48/20000
1000: 2 2/2 2/200 38/20000
libkeyutils C 라이브러리 사용 예:
#include <keyutils.h>
int main(void)
{
key_serial_t key;
/* 세션 키링에 user 타입 키 추가 */
key = add_key("user", "my-api-token",
"Bearer xyz123", 13, KEY_SPEC_SESSION_KEYRING);
if (key < 0) {
perror("add_key");
return 1;
}
/* 키 검색 */
key = request_key("user", "my-api-token",
NULL, KEY_SPEC_SESSION_KEYRING);
/* 키 페이로드 읽기 */
char buf[256];
long len = keyctl_read(key, buf, sizeof(buf));
/* 키 만료 설정 (1시간) */
keyctl_set_timeout(key, 3600);
return 0;
}
request_key 메커니즘
request_key()는 키를 검색하되, 찾지 못하면 사용자 공간의 /sbin/request-key 프로그램을 호출하여 키를 생성하는 upcall 메커니즘입니다. NFS4, CIFS, AFS 등 네트워크 파일시스템(Filesystem)의 인증 토큰 획득에 주로 사용됩니다.
request-key.conf 형식:
# /etc/request-key.conf
# 동작 타입 설명패턴 콜아웃정보 프로그램
create user dns_resolver:* * /sbin/key.dns_resolver %k
create user nfs4:* * /usr/sbin/nfs4_id_to_key %k %d %c %S
create big_key * * /usr/share/keyutils/request-key-debug.sh %k %d %c %S
negate * * * /bin/keyctl negate %k 30 %S
키 생명주기
키는 생성부터 파괴까지 여러 상태를 거칩니다. 가비지 컬렉터(key_gc_work)가 만료, 폐기, 무효화된 키를 자동으로 정리합니다.
/* include/linux/key.h — 키 상태 (key->state) */
#define KEY_IS_UNINSTANTIATED 0 /* 아직 페이로드 없음 */
#define KEY_IS_POSITIVE 1 /* 정상 인스턴스화 */
/* 키 플래그 (key->flags) */
#define KEY_FLAG_DEAD 0 /* 키 타입 해제 → 사용 불가 */
#define KEY_FLAG_REVOKED 1 /* 키 폐기됨 */
#define KEY_FLAG_IN_QUOTA 2 /* 할당량 적용 대상 */
#define KEY_FLAG_USER_CONSTRUCT 3 /* 사용자 공간에서 구성 중 */
#define KEY_FLAG_ROOT_CAN_CLEAR 4 /* root가 키링 비우기 가능 */
#define KEY_FLAG_INVALIDATED 5 /* 즉시 무효화됨 */
#define KEY_FLAG_BUILTIN 6 /* 커널 빌트인 키 */
#define KEY_FLAG_ROOT_CAN_INVAL 7 /* root가 무효화 가능 */
#define KEY_FLAG_KEEP 8 /* GC 보호 (영구 유지) */
키 할당량 (Quota)
키링 서비스는 사용자별 키 할당량을 적용하여 DoS 공격을 방지합니다. 각 사용자(UID)는 최대 키 개수와 최대 바이트 수에 제한을 받습니다.
# 현재 할당량 확인
$ cat /proc/sys/kernel/keys/maxkeys
200
$ cat /proc/sys/kernel/keys/maxbytes
20000
# root의 할당량 (별도 설정)
$ cat /proc/sys/kernel/keys/root_maxkeys
1000000
$ cat /proc/sys/kernel/keys/root_maxbytes
25000000
# 사용자별 현재 사용량 (/proc/key-users)
# UID: nkeys objs/quota bytes/quota
$ cat /proc/key-users
0: 10 10/1000000 500/25000000
1000: 5 5/200 300/20000
| sysctl 매개변수 | 기본값 | 설명 |
|---|---|---|
kernel/keys/maxkeys | 200 | 일반 사용자의 최대 키 개수 |
kernel/keys/maxbytes | 20000 | 일반 사용자의 최대 키 바이트 |
kernel/keys/root_maxkeys | 1000000 | root의 최대 키 개수 |
kernel/keys/root_maxbytes | 25000000 | root의 최대 키 바이트 |
kernel/keys/gc_delay | 300 | GC 실행 지연 시간 (초) |
할당량 초과: 키 생성 시 할당량을 초과하면 -EDQUOT을 반환합니다. 대량의 키를 다루는 서비스(NFS4 등)에서는 KEY_FLAG_IN_QUOTA 플래그를 해제하여 할당량에서 제외하거나, sysctl로 제한을 늘려야 합니다.
키 접근 권한
키 접근 권한은 파일 시스템의 rwx와 유사하지만, 4개의 카테고리에 각 7개의 권한 비트를 사용합니다. 총 30비트(key_perm_t)로 구성됩니다.
| 비트 | 권한 | 설명 |
|---|---|---|
| bit 0 | VIEW | 키 속성 조회 (describe) |
| bit 1 | READ | 키 페이로드 읽기 |
| bit 2 | WRITE | 키 페이로드 갱신, 키링에 링크/언링크 |
| bit 3 | SEARCH | 키링 검색 시 이 키 발견 가능 |
| bit 4 | LINK | 키를 다른 키링에 링크 가능 |
| bit 5 | SETATTR | 속성 변경 (perm, timeout, UID/GID) |
| bit 6 | (예약) | 향후 확장용 |
4개의 카테고리:
| 카테고리 | 비트 위치 | 적용 대상 |
|---|---|---|
| Possessor | 비트 24~30 | 키를 소유(possess)하고 있는 프로세스 |
| User | 비트 16~22 | 키의 UID와 같은 UID를 가진 프로세스 |
| Group | 비트 8~14 | 키의 GID와 같은 GID를 가진 프로세스 |
| Other | 비트 0~6 | 위 어디에도 해당하지 않는 프로세스 |
# 권한 확인
$ keyctl describe %:session
keyring;1000;1000;3f1b0000;_ses
# 권한 해석: 0x3f1b0000
# Possessor: 0x3f = VIEW|READ|WRITE|SEARCH|LINK|SETATTR (전체)
# User: 0x1b = VIEW|READ|SEARCH|LINK
# Group: 0x00 = 없음
# Other: 0x00 = 없음
# 권한 설정 예
$ keyctl setperm 123456 0x3f1f0000 # possessor=all, user=all-setattr
Possessor 개념: "소유(possession)"는 파일의 소유자와 다릅니다. 프로세스의 키링 계층(thread → process → session)에서 검색 가능한 키를 소유하고 있다고 봅니다. 같은 UID이더라도 키가 다른 세션 키링에 있으면 possessor가 아닐 수 있습니다.
Linux 5.4부터 ACL(Access Control List) 기반 접근 제어(Access Control)도 지원합니다:
/* include/linux/key.h — 키 ACL 항목 */
struct key_ace {
unsigned int type; /* KEY_ACE_SUBJ_* */
unsigned int perm; /* KEY_ACE_* 권한 비트 */
union {
kuid_t uid;
kgid_t gid;
};
};
플랫폼 키링
커널은 부팅 시 여러 시스템 키링을 생성하여 Secure Boot, 모듈 서명, IMA 등의 신뢰 앵커로 사용합니다.
| 키링 | 커널 설정 | 내용 | 수정 가능 여부 |
|---|---|---|---|
.builtin_trusted_keys | (기본 내장) | 커널 빌드 시 내장된 X.509 인증서 | 불가 (읽기 전용(Read-Only)) |
.secondary_trusted_keys | CONFIG_SECONDARY_TRUSTED_KEYRING | 빌트인 키로 서명된 인증서 추가 가능 | 제한적 추가 |
.machine | CONFIG_INTEGRITY_MACHINE_KEYRING | MOK(Machine Owner Key) 인증서 | UEFI MOK 관리 |
.platform | CONFIG_INTEGRITY_PLATFORM_KEYRING | UEFI Secure Boot db 인증서 | UEFI 설정에서 관리 |
.blacklist | CONFIG_SYSTEM_BLACKLIST_KEYRING | 거부할 인증서/모듈 해시(Hash) | 제한적 추가 |
.ima | CONFIG_IMA_KEYRINGS_PERMIT_SIGNED_BY_BUILTIN_OR_SECONDARY | IMA appraisal 검증 키 | 서명된 키만 추가 |
# 빌트인 신뢰 키링의 인증서 확인
$ keyctl show %:.builtin_trusted_keys
Keyring
123456 ---lswrv 0 0 keyring: .builtin_trusted_keys
234567 ---lswrv 0 0 \_ asymmetric: Red Hat kernel signing key: ...
345678 ---lswrv 0 0 \_ asymmetric: Fedora kernel signing key 5: ...
# 플랫폼 키링 (UEFI db 인증서)
$ keyctl show %:.platform
Keyring
456789 ---lswrv 0 0 keyring: .platform
567890 ---lswrv 0 0 \_ asymmetric: Microsoft Corporation UEFI CA 2011: ...
fscrypt 키 체인
fscrypt(파일시스템 수준 암호화)는 v2 정책부터 커널 키링 서비스를 통해 키를 관리합니다. 각 파일시스템 슈퍼블록(Superblock)에 .fscrypt 키링이 생성되며, 사용자는 fscryptctl 또는 FS_IOC_ADD_ENCRYPTION_KEY ioctl로 키를 추가합니다.
/* fs/crypto/keyring.c */
/* fscrypt 마스터 키 — 파일시스템 키링에 저장 */
struct fscrypt_master_key {
struct hlist_node mk_node;
struct rw_semaphore mk_sem;
refcount_t mk_struct_refs;
struct rcu_head mk_rcu_head;
struct fscrypt_key_specifier mk_spec;
/* 정책별 파생 키 */
struct fscrypt_prepared_key mk_direct_keys[FSCRYPT_MODE_MAX + 1];
struct fscrypt_prepared_key mk_iv_ino_lblk_64_keys[FSCRYPT_MODE_MAX + 1];
struct fscrypt_prepared_key mk_iv_ino_lblk_32_keys[FSCRYPT_MODE_MAX + 1];
};
# fscrypt v2 정책 설정 흐름
# 1. 마스터 키 생성
$ fscryptctl generate_key > /etc/fscrypt/master.key
# 2. 파일시스템에 키 추가 (ioctl 기반)
$ fscryptctl add_key /mnt/encrypted < /etc/fscrypt/master.key
Added key with identifier: 0x1234567890abcdef
# 3. 디렉토리에 암호화 정책 적용
$ fscryptctl set_policy 0x1234567890abcdef /mnt/encrypted/private/
# 4. 키링에서 확인
$ keyctl show %:.fscrypt
Keyring
789012 ---lswrv 0 0 keyring: .fscrypt
890123 ---lswrv 0 0 \_ fscrypt-provisioning: ...
# 5. 키 제거 (잠금)
$ fscryptctl remove_key /mnt/encrypted 0x1234567890abcdef
v1 vs v2 정책: v1 정책은 사용자 공간에서 키 파생을 수행하고 process keyring에 키를 저장했습니다. v2 정책은 커널 내부에서 HKDF-SHA512로 키를 파생하고, 파일시스템별 .fscrypt 키링에 저장합니다. v2가 보안적으로 더 안전하며, 다중 사용자 환경에서의 키 격리(Isolation)도 개선되었습니다.
dm-crypt 키 연동
dm-crypt는 블록 디바이스 수준 암호화를 제공하며, 커널 키링에서 암호화 키를 가져올 수 있습니다. logon 타입 키를 사용하여 키가 사용자 공간에 노출되는 것을 방지합니다.
# 방법 1: logon 키로 dm-crypt 볼륨 키 제공
# 1. logon 키 생성 (사용자 공간에서 read 불가)
$ keyctl add logon dm-crypt:my-volume "$(dd if=/dev/urandom bs=64 count=1 2>/dev/null)" @s
901234567
# 2. dmsetup으로 디바이스 생성 (키링에서 키 참조)
$ dmsetup create my-crypt --table \
"0 $(blockdev --getsz /dev/sdb) crypt aes-xts-plain64 :64:logon:dm-crypt:my-volume 0 /dev/sdb 0"
# 방법 2: trusted + encrypted 키 체인
# 1. TPM으로 보호되는 마스터 키
$ keyctl add trusted tpm-master "new 32" @s
$ keyctl pipe %:tpm-master > /etc/keys/tpm-master.blob
# 2. encrypted 볼륨 키 (TPM 마스터로 보호)
$ keyctl add encrypted dm-crypt:secure-vol "new trusted:tpm-master 64" @s
$ keyctl pipe %:dm-crypt:secure-vol > /etc/keys/volume.blob
# 3. 부팅 시 키 체인 복원
$ keyctl add trusted tpm-master "load $(cat /etc/keys/tpm-master.blob)" @s
$ keyctl add encrypted dm-crypt:secure-vol "load $(cat /etc/keys/volume.blob)" @s
# 4. cryptsetup으로 볼륨 열기
$ cryptsetup open --type plain --key-file /proc/self/fd/0 --key-size 512 \
/dev/sdb my-crypt <<< "$(keyctl pipe %:dm-crypt:secure-vol)"
/* drivers/md/dm-crypt.c — 키링에서 키 로드 */
static int crypt_set_keyring_key(struct crypt_config *cc,
const char *key_string)
{
struct key *key;
const struct user_key_payload *ukp;
/* key_string 형식: ":size:logon:description" */
key = request_key(&key_type_logon, key_desc, NULL);
if (IS_ERR(key))
return PTR_ERR(key);
down_read(&key->sem);
ukp = user_key_payload_locked(key);
/* ukp->data에서 암호화 키 복사 */
memcpy(cc->key, ukp->data, cc->key_size);
up_read(&key->sem);
key_put(key);
return 0;
}
watch_queue 알림
Linux 5.8에서 도입된 watch_queue는 키 변경 이벤트를 pipe를 통해 사용자 공간에 비동기로 전달합니다. 키의 생성, 갱신, 폐기, 무효화, 만료 이벤트를 감시할 수 있습니다.
/* watch_queue를 사용한 키 감시 (사용자 공간) */
#include <linux/watch_queue.h>
#include <linux/keyctl.h>
int main(void)
{
int pipefd[2];
pipe2(pipefd, O_NOTIFICATION_PIPE);
/* pipe를 watch_queue로 설정 */
ioctl(pipefd[0], IOC_WATCH_QUEUE_SET_SIZE, 256);
/* 세션 키링 감시 시작 */
keyctl_watch_key(KEY_SPEC_SESSION_KEYRING, pipefd[0], 0x01);
/* 이벤트 읽기 루프 */
struct key_notification kn;
while (read(pipefd[0], &kn, sizeof(kn)) > 0) {
printf("Key %u: event %u, aux %u\n",
kn.key_id, kn.subtype, kn.aux);
}
}
커널 설정 종합
| 커널 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_KEYS | Y | Key Retention Service 핵심 (필수) |
CONFIG_KEYS_REQUEST_CACHE | N | request_key 결과 태스크별 캐시(Cache) |
CONFIG_PERSISTENT_KEYRINGS | N | 영구 키링 (UID 네임스페이스 독립) |
CONFIG_TRUSTED_KEYS | M | TPM/TEE sealed 키 타입 |
CONFIG_TRUSTED_KEYS_TPM | Y | TPM2 기반 trusted 키 |
CONFIG_TRUSTED_KEYS_TEE | N | ARM TrustZone TEE 기반 trusted 키 |
CONFIG_TRUSTED_KEYS_CAAM | N | NXP CAAM 기반 trusted 키 |
CONFIG_ENCRYPTED_KEYS | M | 암호화된 키 타입 |
CONFIG_BIG_KEYS | Y | 대용량 키 타입 (shmem 기반) |
CONFIG_KEY_DH_OPERATIONS | N | Diffie-Hellman 키 교환 |
CONFIG_KEY_NOTIFICATIONS | N | watch_queue 키 알림 |
CONFIG_ASYMMETRIC_KEY_TYPE | Y | 비대칭 키 타입 (X.509/PKCS#7) |
CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE | Y | 공개키 하위 타입 |
CONFIG_X509_CERTIFICATE_PARSER | Y | X.509 인증서 파서 |
CONFIG_PKCS7_MESSAGE_PARSER | Y | PKCS#7 메시지 파서 |
CONFIG_SYSTEM_TRUSTED_KEYRING | Y | .builtin_trusted_keys 키링 |
CONFIG_SECONDARY_TRUSTED_KEYRING | N | .secondary_trusted_keys 키링 |
CONFIG_SYSTEM_BLACKLIST_KEYRING | N | .blacklist 키링 |
CONFIG_INTEGRITY_PLATFORM_KEYRING | N | UEFI .platform 키링 |
CONFIG_INTEGRITY_MACHINE_KEYRING | N | MOK .machine 키링 |
# 최소 구성 (기본 키링 + trusted + encrypted)
CONFIG_KEYS=y
CONFIG_TRUSTED_KEYS=m
CONFIG_ENCRYPTED_KEYS=m
CONFIG_BIG_KEYS=y
CONFIG_ASYMMETRIC_KEY_TYPE=y
CONFIG_X509_CERTIFICATE_PARSER=y
CONFIG_SYSTEM_TRUSTED_KEYRING=y
# 완전 구성 (watch_queue + 플랫폼 키링 + DH)
CONFIG_KEYS=y
CONFIG_KEYS_REQUEST_CACHE=y
CONFIG_PERSISTENT_KEYRINGS=y
CONFIG_TRUSTED_KEYS=m
CONFIG_TRUSTED_KEYS_TPM=y
CONFIG_TRUSTED_KEYS_TEE=y
CONFIG_ENCRYPTED_KEYS=m
CONFIG_BIG_KEYS=y
CONFIG_KEY_DH_OPERATIONS=y
CONFIG_KEY_NOTIFICATIONS=y
CONFIG_ASYMMETRIC_KEY_TYPE=y
CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE=y
CONFIG_X509_CERTIFICATE_PARSER=y
CONFIG_PKCS7_MESSAGE_PARSER=y
CONFIG_SYSTEM_TRUSTED_KEYRING=y
CONFIG_SECONDARY_TRUSTED_KEYRING=y
CONFIG_SYSTEM_BLACKLIST_KEYRING=y
CONFIG_INTEGRITY_PLATFORM_KEYRING=y
CONFIG_INTEGRITY_MACHINE_KEYRING=y
커널 내부 구현
키링 서비스의 핵심 내부 구현을 분석합니다. security/keys/ 디렉토리의 주요 흐름을 추적합니다.
키링 내부의 연관 배열(assoc_array) 자료구조:
/* lib/assoc_array.c — 키링의 키 저장소 */
/*
* 키링은 assoc_array를 사용하여 하위 키를 저장합니다.
* 4원 트라이(radix-4 trie) 기반으로, 키의 index_key 해시를
* 2비트씩 분할하여 트리를 구성합니다.
*
* 장점:
* - O(1) 평균 검색/삽입 (해시 기반)
* - RCU 안전한 동시 읽기
* - 메모리 효율적 (노드당 최대 16개 슬롯)
*/
struct assoc_array {
struct assoc_array_ptr *root; /* 루트 노드/리프 포인터 */
unsigned long nr_leaves_on_tree; /* 키 개수 */
};
/* 키링에서 키 검색 (RCU 보호) */
struct key *keyring_search_rcu(struct key *keyring,
struct keyring_index_key *index_key)
{
rcu_read_lock();
/* assoc_array_find()로 해시 기반 O(1) 검색 */
struct assoc_array_ptr *slot;
slot = assoc_array_find(&keyring->keys, &keyring_assoc_array_ops,
index_key);
rcu_read_unlock();
return assoc_array_ptr_to_leaf(slot);
}
키 검색 시 키링 재귀 탐색 과정:
/* security/keys/keyring.c — 재귀 검색 */
struct key *keyring_search(struct key *keyring,
struct key_type *type,
const char *description,
bool recurse)
{
/*
* 검색 알고리즘:
* 1. 현재 키링에서 타입+설명 매칭 키 검색
* 2. 매칭 키 발견 → 접근 권한 체크 → 반환
* 3. recurse=true이면 하위 키링도 재귀 검색
* 4. 순환 참조 방지: 깊이 6 제한
*/
struct keyring_search_context ctx = {
.index_key.type = type,
.index_key.description = description,
.cred = current_cred(),
.match_data.cmp = key_default_cmp,
.match_data.raw_data = description,
.flags = KEYRING_SEARCH_LOOKUP_DIRECT,
};
return search_nested_keyrings(keyring, &ctx);
}
LSM 연동: 키의 모든 주요 동작(생성, 읽기, 검색, 링크 등)에서 LSM 후크(Hook)가 호출됩니다. SELinux의 경우 security_key_alloc(), security_key_permission() 등을 통해 키 접근을 정책으로 제어합니다. 타입 시행(key_create, key_write, key_search 등)은 SELinux 정책 파일에서 정의합니다.
가비지 컬렉터 상세 구현
키링 GC(key_gc_work)는 만료, 폐기, 무효화된 키를 비동기적으로 정리합니다. 워크큐(Workqueue) 기반으로 동작하며, 키 상태 변경 시 스케줄링됩니다.
/* security/keys/gc.c — 키 가비지 컬렉터 핵심 */
/* GC 워크 함수 */
static void key_garbage_collector(struct work_struct *work)
{
static u8 gc_state;
time64_t new_timer, limit, now;
struct key *key;
struct rb_node *cursor;
now = ktime_get_real_seconds();
new_timer = TIME64_MAX;
/* key_serial_tree를 RB 트리 순회 */
spin_lock(&key_serial_lock);
cursor = rb_first(&key_serial_tree);
while (cursor) {
key = rb_entry(cursor, struct key, serial_node);
cursor = rb_next(cursor);
/* GC 보호 키 건너뛰기 */
if (test_bit(KEY_FLAG_KEEP, &key->flags))
continue;
/* dead/revoked/invalidated 키 수거 */
if (test_bit(KEY_FLAG_DEAD, &key->flags) ||
test_bit(KEY_FLAG_INVALIDATED, &key->flags)) {
key_gc_dead_key(key);
continue;
}
/* 만료 키 처리 */
if (key->expiry != 0) {
if (now >= key->expiry) {
/* 만료됨 → 폐기 처리 */
set_bit(KEY_FLAG_DEAD, &key->flags);
key_gc_dead_key(key);
} else {
/* 아직 안 만료 → 다음 GC 타이머 갱신 */
if (key->expiry < new_timer)
new_timer = key->expiry;
}
}
}
spin_unlock(&key_serial_lock);
/* 다음 GC 스케줄링 (가장 빠른 만료 시각 기준) */
if (new_timer != TIME64_MAX)
queue_delayed_work(system_wq, &key_gc_work,
(new_timer - now) * HZ);
}
/* 키 GC 트리거 — 키 폐기/무효화 시 호출 */
void key_schedule_gc(time64_t gc_at)
{
unsigned long expires;
if (gc_at <= ktime_get_real_seconds())
expires = 0; /* 즉시 실행 */
else
expires = (gc_at - ktime_get_real_seconds()) * HZ;
mod_delayed_work(system_wq, &key_gc_work, expires);
}
/* 개별 키 해제 */
static void key_gc_dead_key(struct key *key)
{
/* 1. 키가 속한 모든 키링에서 언링크 */
key_unlink_from_all_keyrings(key);
/* 2. key_type->destroy() 콜백 호출 */
if (key->type->destroy)
key->type->destroy(key);
/* 3. LSM 보안 레이블 해제 */
security_key_free(key);
/* 4. 할당량 반환 */
key_user_put(key->user);
/* 5. serial RB 트리에서 제거 */
rb_erase(&key->serial_node, &key_serial_tree);
/* 6. slab 메모리 해제 */
kmem_cache_free(key_jar, key);
}
키 접근 권한 검사 흐름
key_task_permission()은 키 접근 시 호출되는 핵심 권한 검사 함수입니다. POSIX 파일 권한과 유사하지만 possessor 개념이 추가된 4-카테고리 모델을 사용합니다.
/* security/keys/permission.c — 키 권한 검사 핵심 */
int key_task_permission(
const key_ref_t key_ref,
const struct cred *cred,
enum key_need_perm need_perm)
{
struct key *key = key_ref_to_ptr(key_ref);
key_perm_t perm;
unsigned allow = 0;
/* 1단계: possessor 확인
* key_ref의 하위 비트가 1이면 호출자가 키를 소유함
* (키링 검색 과정에서 결정) */
if (is_key_possessed(key_ref))
allow = key->perm >> 24; /* possessor 비트 */
/* 2단계: UID 매칭 — 키 소유자와 동일 UID */
if (uid_eq(key->uid, cred->fsuid))
allow |= key->perm >> 16; /* user 비트 */
/* 3단계: GID 매칭 — 키 그룹과 동일 GID */
if (gid_eq(key->gid, cred->fsgid) ||
groups_search(cred->group_info, key->gid))
allow |= key->perm >> 8; /* group 비트 */
/* 4단계: other — 위 어디에도 해당하지 않음 */
allow |= key->perm; /* other 비트 */
allow &= 0x7f; /* 하위 7비트만 사용 */
/* 5단계: 요청된 권한 검사 */
if (!(allow & key_need_perm_to_mask(need_perm)))
return -EACCES;
/* 6단계: LSM 권한 검사 (SELinux/AppArmor) */
return security_key_permission(key_ref, cred, need_perm);
}
커널 내부 API
커널 모듈이나 서브시스템에서 키를 관리할 때 사용하는 핵심 내부 API를 상세히 분석합니다. 이 함수들은 security/keys/ 디렉토리에 구현되어 있으며, EXPORT_SYMBOL로 모듈에 공개됩니다.
key_alloc() — 키 할당
key_alloc()은 새 키 객체를 할당하고 초기화합니다. 이 시점에서 키는 아직 인스턴스화되지 않은 상태(uninstantiated)입니다.
/* security/keys/key.c */
struct key *key_alloc(
struct key_type *type, /* 키 타입 */
const char *desc, /* 키 설명(이름) */
kuid_t uid, /* 소유자 UID */
kgid_t gid, /* 소유자 GID */
const struct cred *cred, /* 호출자 자격증명 */
key_perm_t perm, /* 접근 권한 */
unsigned long flags, /* KEY_ALLOC_* 플래그 */
struct key_restriction *restrict_link /* 링크 제한 */
)
{
struct key *key;
int ret;
/* 1. 키 타입 유효성 검사 */
if (!type)
return ERR_PTR(-EINVAL);
/* 2. 할당량 검사 (KEY_ALLOC_NOT_IN_QUOTA가 없으면) */
if (!(flags & KEY_ALLOC_NOT_IN_QUOTA)) {
ret = key_payload_reserve(key, type->def_datalen);
if (ret < 0)
return ERR_PTR(ret); /* -EDQUOT */
}
/* 3. struct key 메모리 할당 */
key = kmem_cache_zalloc(key_jar, GFP_KERNEL);
/* 4. 일련번호 할당 (RB 트리 삽입) */
key->serial = key_alloc_serial(key);
/* 5. LSM 보안 레이블 할당 */
ret = security_key_alloc(key, cred, flags);
/* 6. 기본 필드 초기화 */
key->type = type;
key->uid = uid;
key->gid = gid;
key->perm = perm;
refcount_set(&key->usage, 1);
return key;
}
| 플래그 | 값 | 설명 |
|---|---|---|
KEY_ALLOC_IN_QUOTA | 0x0000 | 할당량 적용 (기본) |
KEY_ALLOC_NOT_IN_QUOTA | 0x0002 | 할당량 면제 (커널 내부 키) |
KEY_ALLOC_BUILT_IN | 0x0004 | 빌트인 키 (부팅 시 생성) |
KEY_ALLOC_BYPASS_RESTRICTION | 0x0008 | 링크 제한 우회 |
KEY_ALLOC_UID_KEYRING | 0x0010 | UID 키링 생성 |
KEY_ALLOC_SET_KEEP | 0x0020 | GC 보호 설정 |
key_instantiate_and_link() — 키 인스턴스화
할당된 키에 페이로드를 설정하고 대상 키링에 링크합니다. request_key upcall의 최종 단계에서도 이 함수가 호출됩니다.
/* security/keys/key.c */
int key_instantiate_and_link(
struct key *key, /* 인스턴스화할 키 */
const void *data, /* 페이로드 데이터 */
size_t datalen, /* 데이터 길이 */
struct key *keyring, /* 링크할 대상 키링 */
struct key *authkey /* 인증 키 (upcall 시) */
)
{
struct key_preparsed_payload prep;
int ret;
/* 1. preparse: key_type->preparse() 호출 */
prep.data = data;
prep.datalen = datalen;
if (key->type->preparse) {
ret = key->type->preparse(&prep);
if (ret < 0)
return ret;
}
/* 2. 페이로드 할당량 갱신 */
key_payload_reserve(key, prep.quotalen);
/* 3. instantiate: key_type->instantiate() 호출 */
ret = key->type->instantiate(key, &prep);
/* 4. 키링에 링크 */
if (keyring)
__key_link(key, keyring);
/* 5. 대기 중인 request_key() 호출자 깨우기 */
mark_key_instantiated(key, 0);
wake_up_bit(&key->flags, KEY_FLAG_USER_CONSTRUCT);
return 0;
}
request_key_and_link() — 검색 + upcall + 링크
request_key_and_link()은 커널 내부에서 가장 범용적인 키 검색 함수입니다. 키를 찾지 못하면 upcall을 수행하고, 결과를 대상 키링에 자동 링크합니다.
/* security/keys/request_key.c */
struct key *request_key_and_link(
struct key_type *type,
const char *description,
struct key_tag *domain_tag,
const char *callout_info,
size_t callout_len,
void *aux,
struct key *dest_keyring,
unsigned long flags
)
{
/* 검색 순서:
* 1. 요청 캐시 (CONFIG_KEYS_REQUEST_CACHE)
* 2. thread keyring
* 3. process keyring
* 4. session keyring (재귀)
* 5. user/user_session keyring
* 6. callout_info가 있으면 upcall
*/
struct key *key;
key = search_process_keyrings(type, description, ...);
if (!IS_ERR(key))
return key;
/* 키 미발견 → upcall 수행 */
if (callout_info)
key = construct_key_and_link(...);
return key;
}
key_validate() — 키 유효성 검사
/* security/keys/key.c — 키 사용 전 유효성 확인 */
int key_validate(const struct key *key)
{
unsigned long flags = READ_ONCE(key->flags);
time64_t now = ktime_get_real_seconds();
/* 폐기된 키 */
if (flags & (1 << KEY_FLAG_REVOKED))
return -EKEYREVOKED;
/* 만료된 키 */
if (key->expiry && now >= key->expiry)
return -EKEYEXPIRED;
/* 무효화된 키 */
if (flags & (1 << KEY_FLAG_INVALIDATED))
return -ENOKEY;
return 0;
}
커널 모듈에서 키 활용 실전 패턴
/* security/keys/key.c 기반 예제 — 커널 모듈에서 키 검색, 사용, 해제 패턴 */
static int my_module_get_secret(char *buf, size_t buflen)
{
struct key *key;
const struct user_key_payload *upayload;
int ret;
/* 1. 키 검색 (upcall 없이) */
key = request_key(&key_type_user,
"my-module:secret", NULL);
if (IS_ERR(key))
return PTR_ERR(key);
/* 2. 키 유효성 검사 */
ret = key_validate(key);
if (ret < 0)
goto out;
/* 3. 키 세마포어 잠금 (읽기) */
down_read(&key->sem);
/* 4. 페이로드 접근 (RCU 보호) */
upayload = user_key_payload_locked(key);
if (!upayload || upayload->datalen > buflen) {
ret = -EINVAL;
} else {
memcpy(buf, upayload->data, upayload->datalen);
ret = upayload->datalen;
}
up_read(&key->sem);
out:
/* 5. 키 참조 해제 */
key_put(key);
return ret;
}
네트워크 인증 키
키링 서비스는 네트워크 파일시스템(NFS, CIFS, AFS)과 DNS 해석, TLS 핸드셰이크(Handshake) 등에서 인증 자격증명을 관리하는 데 핵심적으로 사용됩니다.
dns_resolver 키 타입
dns_resolver 키 타입은 커널이 호스트명을 IP 주소로 해석해야 할 때 사용합니다. AFS, CIFS 등 커널 네트워크 파일시스템이 서버 주소를 해석할 때 request_key() upcall로 DNS 조회를 수행합니다.
/* net/dns_resolver/dns_key.c */
struct key_type key_type_dns_resolver = {
.name = "dns_resolver",
.flags = KEY_TYPE_NET_DOMAIN,
.preparse = dns_resolver_preparse,
.free_preparse = dns_resolver_free_preparse,
.instantiate = generic_key_instantiate,
.describe = dns_resolver_describe,
.read = dns_resolver_read,
.destroy = dns_resolver_destroy,
.match_preparse = dns_resolver_match_preparse,
.cmp = dns_resolver_cmp,
};
/* 커널 내부에서 DNS 조회 */
struct key *dns_key = request_key_net(
&key_type_dns_resolver,
"afsdb:*.example.com", /* DNS 질의 */
net, /* 네트워크 네임스페이스 */
"afsdb" /* callout 정보 */
);
# /etc/request-key.conf에 DNS 핸들러 설정
create dns_resolver * * /usr/sbin/key.dns_resolver %k
# DNS 키 수동 확인
$ keyctl request dns_resolver "afsdb:example.com" "" @s
$ keyctl show @s
Session Keyring
... dns_resolver: afsdb:example.com → 192.168.1.100
NFS4 인증 키
NFS4에서는 Kerberos 인증이 필요한 경우 키링을 통해 자격증명을 관리합니다. request_key() upcall로 nfs4_id_to_key 핸들러(Handler)가 호출되어 사용자 ID를 Kerberos 주체(principal)로 매핑합니다.
# NFS4 ID 매핑 키 핸들러 설정
# /etc/request-key.d/id_resolver.conf
create id_resolver * * /usr/sbin/nfsidmap -t 600 %k %d
# NFS4 Kerberos 키탭 기반 인증 흐름
# 1. gssproxy가 Kerberos 티켓을 관리
$ systemctl enable gssproxy
$ systemctl start gssproxy
# 2. NFS 마운트 시 sec=krb5 사용
$ mount -t nfs4 -o sec=krb5 server:/export /mnt/nfs
# 3. 커널이 request_key()로 인증 토큰 요청
# → gssproxy가 키탭에서 TGT 획득 → 키링에 캐싱
# 4. 캐싱된 키 확인
$ keyctl show @s
Session Keyring
... user: nfs4:krb5cc_1000 → [Kerberos credential cache]
CIFS/SMB 인증 키
# CIFS 마운트 시 키링 기반 비밀번호 관리
# 1. 비밀번호를 키링에 저장 (세션 키링)
$ cifscreds add server.example.com
Password: ****
# 2. 내부적으로 logon 키 타입으로 저장됨
$ keyctl show @s
Session Keyring
... logon: cifs:username@server.example.com
# 3. 마운트 시 키링에서 자동 검색
$ mount -t cifs //server.example.com/share /mnt/cifs -o sec=ntlmssp
# 4. 비밀번호 갱신
$ cifscreds update server.example.com
커널 TLS 핸드셰이크 키
Linux 6.2부터 커널 TLS(kTLS) 핸드셰이크가 지원됩니다. 키링 서비스는 TLS 세션 키와 인증서를 관리하는 데 사용됩니다.
/* net/handshake/tlshd.c — 커널 TLS 핸드셰이크 */
/* TLS 핸드셰이크 완료 후 세션 키를 키링에 저장 */
static int tls_handshake_done(struct sock *sk,
struct tls_handshake_args *args)
{
struct key *peerid;
/* 피어 인증서를 asymmetric 키로 저장 */
peerid = key_alloc(&key_type_asymmetric,
"tls:peer-cert", ...);
/* PSK(Pre-Shared Key) 모드에서는 user 키 사용 */
args->ta_keyring = request_key(
&key_type_keyring, "_tls", NULL);
return 0;
}
| 네트워크 서비스 | 키 타입 | 핸들러 | 용도 |
|---|---|---|---|
| NFS4 | user (id_resolver) | nfsidmap | UID/GID → 이름 매핑 |
| NFS4 Kerberos | rxrpc | gssproxy | Kerberos TGT 캐싱 |
| CIFS/SMB | logon (cifs) | cifscreds | SMB 비밀번호 캐싱 |
| AFS | rxrpc | afs_settoken | AFS 토큰 관리 |
| DNS | dns_resolver | key.dns_resolver | 커널 DNS 해석 |
| kTLS | asymmetric / user | tlshd | TLS 세션 키/인증서 |
TPM 연동
TPM(Trusted Platform Module)과 키링의 연동은 키 보호의 최고 수준을 제공합니다. trusted 키 타입의 seal/unseal 흐름, PCR 바인딩, 정책 기반 봉인을 상세히 분석합니다.
trusted 키 생성과 PCR 바인딩
# TPM2 trusted 키 — 기본 생성
$ keyctl add trusted my-tpm-key "new 32" @s
123456789
# PCR 바인딩으로 생성 (PCR 0,2,7에 바인딩)
$ keyctl add trusted sealed-key "new 32 pcrinfo=0:sha256=...,2:sha256=...,7:sha256=..." @s
# sealed blob을 디스크에 저장
$ keyctl pipe 123456789 > /etc/keys/my-tpm-key.blob
# 재부팅 후 키 복원 (unseal)
$ keyctl add trusted my-tpm-key "load $(cat /etc/keys/my-tpm-key.blob)" @s
# PCR 값이 변경된 경우 (예: 커널 업데이트 후)
$ keyctl add trusted my-tpm-key "load $(cat /etc/keys/my-tpm-key.blob)" @s
# → -EPERM: PCR 불일치로 unseal 실패
# 키 갱신 (새 PCR 값으로 재봉인)
$ keyctl add trusted my-tpm-key "update pcrinfo=0:sha256=..." @s
$ keyctl pipe 123456789 > /etc/keys/my-tpm-key.blob # 새 blob 저장
trusted + encrypted 키 체인
가장 안전한 키 보호 구성은 TPM trusted 키를 마스터로 사용하고, encrypted 키로 실제 볼륨 키를 보호하는 이중 구조입니다.
TPM → trusted → encrypted → dm-crypt 키 체인
보호 계층:
[TPM SRK] ──seal──→ [trusted 키 (32B 마스터)]
│
AES-GCM 래핑
│
▼
[encrypted 키 (64B 볼륨 키)]
│
▼
[dm-crypt 블록 암호화]
공격자가 디스크만 탈취한 경우:
- encrypted blob만 존재합니다 (평문 없음).
- 마스터 키는 TPM에 봉인되어 있습니다.
- TPM 없이는 마스터 키를 추출할 수 없습니다.
- PCR 바인딩 시 부팅 환경 변경도 차단됩니다.
# 전체 키 체인 설정 예시
# 1단계: TPM trusted 마스터 키
$ keyctl add trusted tpm-master "new 32 pcrinfo=0,7" @s
$ keyctl pipe %:tpm-master > /etc/keys/tpm-master.blob
$ chmod 400 /etc/keys/tpm-master.blob
# 2단계: encrypted 볼륨 키 (trusted 마스터로 보호)
$ keyctl add encrypted vol-key "new trusted:tpm-master 64" @s
$ keyctl pipe %:vol-key > /etc/keys/vol-key.blob
$ chmod 400 /etc/keys/vol-key.blob
# 3단계: dm-crypt 볼륨에 적용
$ dmsetup create secure-vol --table \
"0 $(blockdev --getsz /dev/sdb) crypt \
aes-xts-plain64 :64:logon:dm-crypt:vol-key 0 /dev/sdb 0"
# 부팅 시 자동 복원 스크립트 (/etc/initramfs-tools/scripts/local-top/)
#!/bin/sh
keyctl add trusted tpm-master "load $(cat /etc/keys/tpm-master.blob)" @s
keyctl add encrypted vol-key "load $(cat /etc/keys/vol-key.blob)" @s
키 보안과 감사
키링 서비스의 보안 모델은 다층적 접근 제어와 감사(Audit) 기능을 포함합니다. 키의 생성부터 파괴까지 모든 과정에서 보안 검증이 수행됩니다.
LSM 보안 후크
키의 모든 주요 동작에서 LSM(Linux Security Module) 후크가 호출됩니다. SELinux, AppArmor 등의 보안 모듈은 이 후크를 통해 키 접근 정책을 시행합니다.
| LSM 후크 | 호출 시점 | SELinux 권한 |
|---|---|---|
security_key_alloc() | 키 할당 시 | key { create } |
security_key_free() | 키 해제 시 | - |
security_key_permission() | 키 접근 시 | key { view read write search link setattr } |
security_key_getsecurity() | 보안 레이블 조회 | - |
SELinux 키 접근 정책 예시
# SELinux 정책에서 키 접근 제어
# /etc/selinux/policy/modules/my_key_policy.te
# 특정 도메인이 user 타입 키를 생성/검색 가능
allow my_app_t user_key_t:key { create write view read search link };
# 특정 도메인의 trusted 키 접근 차단
neverallow untrusted_app_t trusted_key_t:key *;
# 키 접근 감사 로깅
auditallow my_app_t user_key_t:key { write };
커널 감사 이벤트
커널 감사 서브시스템은 키 관련 이벤트를 기록합니다. auditctl로 키 감사 규칙을 설정할 수 있습니다.
# 키 생성/삭제 감사 규칙
$ auditctl -a always,exit -S add_key -k key_create
$ auditctl -a always,exit -S keyctl -k key_control
# 감사 로그 확인
$ ausearch -k key_create
type=SYSCALL msg=audit(1234567890.123:456):
arch=c000003e syscall=248 success=yes exit=789012345
a0=7f1234 a1=7f5678 a2=7f9abc a3=fffffffe
key="key_create"
# 키 관련 감사 요약
$ aureport --key --summary
키 보안 모범 사례
| 항목 | 권장 사항 | 이유 |
|---|---|---|
| 키 타입 선택 | 비밀 데이터는 logon 타입 사용 | 사용자 공간 read 차단 |
| 만료 설정 | 모든 임시 키에 만료 시간 설정 | 키 누적 방지, DoS 예방 |
| TPM 활용 | 장기 비밀은 trusted 키 사용 | 하드웨어 보호, cold boot 방어 |
| 권한 최소화 | 필요한 최소 권한만 설정 | Possessor 외 접근 차단 |
| 키링 분리 | 서비스별 별도 키링 사용 | 키 격리, 범위 제한 |
| 감사 로깅 | 중요 키 작업 감사 활성화 | 사고 추적, 컴플라이언스 |
| 키 순환 | 정기적 키 갱신 자동화 | 장기 사용 키 탈취 위험 감소 |
실전 예제 종합
키링 서비스를 활용한 실전 시나리오와 코드 예제를 종합합니다.
시나리오 1: 애플리케이션 시크릿 관리
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <keyutils.h>
/* 데이터베이스 비밀번호를 키링에 안전하게 저장하고 사용하는 예제 */
int store_db_password(const char *db_name, const char *password) {
char desc[256];
key_serial_t key;
/* logon 키로 저장 → 사용자 공간에서 read 불가 */
snprintf(desc, sizeof(desc), "myapp:db:%s", db_name);
key = add_key("logon", desc,
password, strlen(password),
KEY_SPEC_SESSION_KEYRING);
if (key < 0) {
perror("add_key");
return -1;
}
/* 1시간 후 만료 설정 */
keyctl_set_timeout(key, 3600);
/* 접근 권한 설정: possessor만 접근, 다른 사용자 차단 */
keyctl_setperm(key, 0x3f000000);
printf("키 저장 완료: serial=%d\n", key);
return key;
}
int retrieve_api_token(const char *service, char *buf, size_t len) {
char desc[256];
key_serial_t key;
long ret;
/* user 타입 키 검색 (read 가능) */
snprintf(desc, sizeof(desc), "myapp:api:%s", service);
key = request_key("user", desc,
NULL, KEY_SPEC_SESSION_KEYRING);
if (key < 0) {
perror("request_key");
return -1;
}
/* 키 페이로드 읽기 */
ret = keyctl_read(key, buf, len);
if (ret < 0) {
perror("keyctl_read");
return -1;
}
return (int)ret;
}
int main(void) {
char token[512];
/* API 토큰 저장 */
add_key("user", "myapp:api:github",
"ghp_xxxxxxxxxxxxxxxxxxxx", 24,
KEY_SPEC_SESSION_KEYRING);
/* 토큰 검색 및 사용 */
int len = retrieve_api_token("github", token, sizeof(token));
if (len > 0)
printf("Token: %.*s\n", len, token);
return 0;
}
# 컴파일 및 실행
$ gcc -o key-demo key-demo.c -lkeyutils
$ ./key-demo
키 저장 완료: serial=123456789
Token: ghp_xxxxxxxxxxxxxxxxxxxx
시나리오 2: systemd 서비스 키 관리
# systemd 서비스에서 키링 활용
# 1. 서비스 시작 전 키를 세션 키링에 주입
# /etc/systemd/system/myapp.service.d/keys.conf
[Service]
ExecStartPre=/usr/bin/keyctl add user myapp:db-password "$(cat /etc/myapp/db.key)" @s
ExecStartPre=/usr/bin/keyctl timeout %%:myapp:db-password 7200
# 2. 애플리케이션은 키링에서 비밀번호 검색
# → /etc/myapp/db.key 파일은 부팅 후 삭제 가능
# 3. 키 순환 자동화 (cron)
# /etc/cron.daily/rotate-myapp-keys
#!/bin/bash
NEW_PASS=$(openssl rand -base64 32)
keyctl update %:myapp:db-password "$NEW_PASS"
# DB 비밀번호도 함께 변경하는 로직 필요
시나리오 3: SSH 에이전트 대체
# 커널 키링을 SSH 키 캐싱에 활용
# 1. SSH 키를 사용자 세션 키링에 저장
$ keyctl add user ssh:id_rsa "$(cat ~/.ssh/id_rsa)" @us
$ keyctl timeout %:ssh:id_rsa 28800 # 8시간 만료
# 2. 접근 권한: 소유자만 읽기 가능
$ keyctl setperm %:ssh:id_rsa 0x3f030000
# 3. 키 목록 확인
$ keyctl show @us
Session Keyring
... user: ssh:id_rsa [8시간 후 만료]
# 4. 키 폐기 (로그아웃 시)
$ keyctl revoke %:ssh:id_rsa
시나리오 4: watch_queue 기반 키 모니터링 도구
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <keyutils.h>
#include <linux/watch_queue.h>
/* 키 변경 이벤트를 실시간 감시하는 모니터링 도구 */
int main(void)
{
int pipefd[2];
struct watch_notification_filter filter;
/* watch_queue pipe 생성 */
if (pipe2(pipefd, O_NOTIFICATION_PIPE) < 0) {
perror("pipe2");
return 1;
}
/* 큐 크기 설정 */
ioctl(pipefd[0], IOC_WATCH_QUEUE_SET_SIZE, 256);
/* 필터: 모든 키 이벤트 수신 */
filter.nr_filters = 1;
filter.__reserved = 0;
filter.filters[0].type = WATCH_TYPE_KEY_NOTIFY;
filter.filters[0].subtype_filter[0] = 0xFFFFFFFF;
ioctl(pipefd[0], IOC_WATCH_QUEUE_SET_FILTER, &filter);
/* 세션 키링 감시 시작 */
keyctl_watch_key(KEY_SPEC_SESSION_KEYRING, pipefd[0], 0x01);
printf("키 이벤트 감시 중...\n");
/* 이벤트 읽기 루프 */
struct key_notification kn;
while (read(pipefd[0], &kn, sizeof(kn)) == sizeof(kn)) {
const char *event_names[] = {
"instantiated", "updated", "linked",
"unlinked", "cleared", "revoked",
"invalidated", "setattr"
};
printf("[KEY EVENT] key=%u event=%s aux=%u\n",
kn.key_id,
event_names[kn.subtype],
kn.aux);
}
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
키링 운영 요약 체크리스트
| 점검 항목 | 명령어 | 기대 결과 |
|---|---|---|
| 커널 키링 지원 확인 | grep CONFIG_KEYS /boot/config-$(uname -r) | CONFIG_KEYS=y |
| 현재 키 목록 | cat /proc/keys | 키 serial, 타입, 설명 표시 |
| 사용자별 키 통계 | cat /proc/key-users | UID별 키 수/할당량 |
| 세션 키링 트리 | keyctl show @s | 계층적 키링 구조 |
| 빌트인 신뢰 키 | keyctl show %:.builtin_trusted_keys | X.509 인증서 목록 |
| TPM 키 지원 | grep TRUSTED_KEYS /boot/config-$(uname -r) | CONFIG_TRUSTED_KEYS=m |
| 할당량 설정 | sysctl kernel.keys.maxkeys | 200 (기본) |
| keyutils 설치 | keyctl --version | 버전 출력 |
Diffie-Hellman 키 교환
Linux 4.7부터 키링 서비스는 커널 공간에서 Diffie-Hellman(DH) 키 교환을 지원합니다(CONFIG_KEY_DH_OPERATIONS). 사용자 공간의 비밀 키를 노출하지 않고 공유 비밀을 안전하게 계산할 수 있습니다. NFS4 Kerberos와 같은 프로토콜(Protocol)에서 세션 키 유도에 활용됩니다.
/* security/keys/dh.c — DH 키 교환 커널 구현 */
/* KEYCTL_DH_COMPUTE 처리 */
long keyctl_dh_compute(
struct keyctl_dh_params __user *_params,
char __user *buffer,
size_t buflen,
struct keyctl_kdf_params __user *_kdf)
{
struct keyctl_dh_params params;
struct key *private_key, *prime_key, *base_key;
MPI result, private_mpi, prime_mpi, base_mpi;
copy_from_user(¶ms, _params, sizeof(params));
/* 1. 키링에서 DH 매개변수 키 검색 */
private_key = key_ref_to_ptr(
lookup_user_key(params.private, 0, KEY_NEED_READ));
prime_key = key_ref_to_ptr(
lookup_user_key(params.prime, 0, KEY_NEED_READ));
base_key = key_ref_to_ptr(
lookup_user_key(params.base, 0, KEY_NEED_READ));
/* 2. 키 페이로드를 MPI(다중 정밀도 정수)로 변환 */
private_mpi = mpi_read_raw_from_sgl(...);
prime_mpi = mpi_read_raw_from_sgl(...);
base_mpi = mpi_read_raw_from_sgl(...);
/* 3. DH 계산: result = base^private mod prime */
result = mpi_alloc(0);
mpi_powm(result, base_mpi, private_mpi, prime_mpi);
/* 4. KDF 적용 (선택적 — _kdf가 NULL이 아닌 경우) */
if (_kdf) {
/* HKDF-SHA256 또는 SP800-56A KDF */
kdf_ctr(kdf_sdesc, derived_buf, derived_keylen,
result_buf, result_len, ...);
}
/* 5. 결과를 사용자 공간으로 복사 */
copy_to_user(buffer, result_buf, buflen);
mpi_free(result);
return buflen;
}
# DH 키 교환 실습 (keyctl 사용)
# 1. DH 소수(prime)와 생성자(generator) 키 저장
# RFC 3526 그룹 14 (2048비트 MODP)
$ keyctl add user dh:prime "$(python3 -c "
import binascii
p = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC7402...
print(binascii.unhexlify(hex(p)[2:]).decode('latin-1'))")" @s
DH_PRIME=$?
# 생성자 g=2
$ keyctl add user dh:generator "$(printf '\x02')" @s
DH_GEN=$?
# 2. 비밀 키 생성 (32바이트 랜덤)
$ keyctl add user dh:private "$(dd if=/dev/urandom bs=32 count=1 2>/dev/null)" @s
DH_PRIV=$?
# 3. 공개 값 계산: g^private mod prime
$ keyctl dh_compute $DH_PRIV $DH_PRIME $DH_GEN > /tmp/my_public.bin
# 4. 상대방의 공개 값을 받아 키링에 저장
$ keyctl add user dh:peer_public "$(cat /tmp/peer_public.bin)" @s
DH_PEER=$?
# 5. 공유 비밀 계산: peer_public^private mod prime
$ keyctl dh_compute $DH_PRIV $DH_PRIME $DH_PEER > /tmp/shared_secret.bin
# 6. KDF 적용하여 세션 키 유도
$ keyctl dh_compute_kdf $DH_PRIV $DH_PRIME $DH_PEER 32 sha256 > /tmp/session_key.bin
| KEYCTL_DH_COMPUTE 매개변수 | 타입 | 설명 |
|---|---|---|
private | key_serial_t | 비밀 키 serial (지수(Exponent)) |
prime | key_serial_t | 소수 p serial (모듈러스) |
base | key_serial_t | 밑 값 serial (generator 또는 상대 공개값) |
kdf.hashname | char * | KDF 해시 알고리즘 ("sha256", "sha512") |
kdf.otherinfolen | __u32 | KDF 추가 정보(OtherInfo) 길이 |
영구 키링 (Persistent Keyrings)
Linux 3.13에서 도입된 영구 키링(CONFIG_PERSISTENT_KEYRINGS)은 사용자의 세션 종료 후에도 유지되는 키링입니다. 일반 세션 키링은 마지막 세션이 종료되면 파괴되지만, 영구 키링은 별도의 만료 타이머(Timer)에 의해 관리되어 재로그인 시에도 키를 유지할 수 있습니다.
/* security/keys/persistent.c — 영구 키링 구현 */
/* 영구 키링 획득 또는 생성 */
long keyctl_get_persistent(uid_t uid, key_serial_t destid)
{
struct key *persistent;
struct key *dest_keyring;
struct user_namespace *ns = current_user_ns();
/* 1. 대상 키링 조회 */
dest_keyring = key_ref_to_ptr(
lookup_user_key(destid, KEY_LOOKUP_CREATE, KEY_NEED_WRITE));
/* 2. UID에 해당하는 영구 키링 검색 또는 생성 */
persistent = key_lookup_persistent(ns, uid);
if (IS_ERR(persistent)) {
/* 없으면 새로 생성 */
persistent = keyring_alloc(
"_persistent", uid, INVALID_GID,
current_cred(),
(KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ),
KEY_ALLOC_NOT_IN_QUOTA, NULL, NULL);
}
/* 3. 영구 키링을 대상 키링에 링크 */
key_link(dest_keyring, persistent);
/* 4. 만료 타이머 리셋 (persistent_keyring_expiry 초) */
persistent->expiry = ktime_get_real_seconds() +
persistent_keyring_expiry;
key_put(persistent);
return persistent->serial;
}
/* sysctl: 영구 키링 기본 만료 시간 (초) */
unsigned persistent_keyring_expiry = 3 * 24 * 60 * 60; /* 3일 */
# 영구 키링 활용
# 1. 영구 키링을 세션 키링에 연결
$ keyctl get_persistent @s
456789012
# 2. 영구 키링에 키 저장 (세션 종료 후에도 유지)
$ keyctl add user nfs4:krb5cc "kerberos-ticket-data" 456789012
$ keyctl show 456789012
Keyring
456789012 ---lswrv 1000 65534 keyring: _persistent
567890123 --alswrv 1000 1000 \_ user: nfs4:krb5cc
# 3. 로그아웃 후 재로그인 → 영구 키링 재연결
$ keyctl get_persistent @s # 동일한 키링이 반환됨
# 4. 만료 시간 확인/변경
$ cat /proc/sys/kernel/keys/persistent_keyring_expiry
259200 # 3일 (초)
$ echo 604800 > /proc/sys/kernel/keys/persistent_keyring_expiry # 7일로 변경
# 5. PAM 연동 — 로그인 시 자동으로 영구 키링 연결
# /etc/pam.d/common-auth에 추가:
# session optional pam_keyinit.so force revoke
# session optional pam_keyctl_get_persistent.so
| sysctl 매개변수 | 기본값 | 설명 |
|---|---|---|
kernel/keys/persistent_keyring_expiry | 259200 (3일) | 영구 키링 비활동 만료 시간 (초). 접근할 때마다 리셋 |
사용 시나리오: 영구 키링은 NFS4 Kerberos 인증에서 특히 유용합니다. 사용자가 SSH 세션을 종료하고 재접속할 때, Kerberos TGT(Ticket-Granting Ticket)가 여전히 영구 키링에 남아 있으므로 재인증 없이 NFS 마운트(Mount)에 접근할 수 있습니다. cron 작업이 사용자 세션 없이 실행될 때에도 영구 키링의 인증 토큰을 사용할 수 있습니다.
키 제한 메커니즘 (Restrict Keyring)
Linux 4.12에서 도입된 키링 제한(KEYCTL_RESTRICT_KEYRING)은 키링에 추가할 수 있는 키의 종류를 제한합니다. 이를 통해 신뢰 체인(Chain of Trust)의 무결성(Integrity)을 보장하고, 무단 키 삽입을 방지합니다. .builtin_trusted_keys와 .secondary_trusted_keys 같은 시스템 키링은 이 메커니즘을 사용하여 서명 검증된 키만 추가할 수 있도록 제한합니다.
/* include/linux/key.h — 키 제한 구조체 */
struct key_restriction {
key_restrict_link_func_t check; /* 링크 허용 여부 검사 함수 */
struct key *key; /* 검증에 사용할 키/키링 */
struct key_type *keytype; /* 허용할 키 타입 */
};
/* 제한 검사 함수 — 키를 키링에 링크하기 전 호출 */
typedef int (*key_restrict_link_func_t)(
struct key *dest_keyring,
const struct key_type *type,
const union key_payload *payload,
struct key *restriction_key);
/* security/keys/keyring.c — 제한 검사가 포함된 __key_link */
int __key_link_check_restriction(
struct key *keyring,
struct key *key)
{
struct key_restriction *restrict_link;
restrict_link = keyring->restrict_link;
if (!restrict_link)
return 0; /* 제한 없음 → 허용 */
/* 제한 검사 함수 호출 */
return restrict_link->check(
keyring,
key->type,
&key->payload,
restrict_link->key);
}
asymmetric 키 타입에서 제공하는 주요 제한 함수:
| 제한 종류 | 함수 | 설명 |
|---|---|---|
| builtin_trusted | restrict_link_by_builtin_trusted() | .builtin_trusted_keys로 서명 검증된 키만 허용 |
| builtin_and_secondary | restrict_link_by_builtin_and_secondary_trusted() | 빌트인 또는 이미 등록된 secondary 키로 서명 검증된 키 허용 |
| key_or_keyring | restrict_link_by_key_or_keyring() | 지정된 키 또는 키링 내 키로 서명 검증된 키만 허용 |
| key_or_keyring_chain | restrict_link_by_key_or_keyring_chain() | 체인 검증 포함 (CA 인증서 체인 지원) |
/* certs/system_keyring.c — .secondary_trusted_keys 제한 설정 */
static int __init system_trusted_keyring_init(void)
{
struct key_restriction *restriction;
/* .builtin_trusted_keys: 빌트인 키만 포함 (외부 추가 불가) */
builtin_trusted_keys = keyring_alloc(
".builtin_trusted_keys", ...
KEY_ALLOC_NOT_IN_QUOTA | KEY_ALLOC_SET_KEEP,
NULL, NULL);
/* .secondary_trusted_keys: 빌트인/secondary로 서명된 키만 추가 가능 */
restriction = kzalloc(sizeof(*restriction), GFP_KERNEL);
restriction->check =
restrict_link_by_builtin_and_secondary_trusted;
secondary_trusted_keys = keyring_alloc(
".secondary_trusted_keys", ...
KEY_ALLOC_NOT_IN_QUOTA | KEY_ALLOC_SET_KEEP,
restriction, NULL);
/* 이 시점 이후 .secondary에는 서명 검증된 키만 추가 가능 */
return 0;
}
# 사용자 공간에서 키링 제한 설정
# 1. 사용자 정의 키링 생성
$ keyctl newring my-trusted @s
RING_ID=$?
# 2. 키링에 제한 설정 — asymmetric 키만 허용, 특정 키로 서명 검증
$ keyctl restrict_keyring $RING_ID asymmetric "key_or_keyring:$SIGNER_KEY_ID"
# 3. 이후 서명 검증에 실패하는 키 추가 시도 → 거부
$ keyctl padd asymmetric "" $RING_ID < unsigned_cert.der
add_key: Permission denied
# 4. 서명 검증 통과하는 키만 추가 가능
$ keyctl padd asymmetric "" $RING_ID < signed_cert.der
890123456 # 성공
# 5. 한번 제한이 설정되면 해제할 수 없음 (보안상)
$ keyctl restrict_keyring $RING_ID # 이미 제한됨 → -EPERM
제한의 비가역성: 키링에 제한이 한 번 설정되면 해제하거나 변경할 수 없습니다. 이는 의도적인 보안 설계입니다. 공격자가 키링 제한을 우회하여 악성 키를 삽입하는 것을 방지합니다. .builtin_trusted_keys 키링은 KEY_FLAG_KEEP이 설정되어 있어 삭제도 불가능합니다.
키 네임스페이스와 도메인 태그
Linux 5.3에서 도입된 키 도메인 태그(domain tag)는 네트워크 네임스페이스(Network Namespace) 등에 의한 키 격리를 구현합니다. 같은 타입과 설명을 가진 키라도 도메인 태그가 다르면 별개의 키로 취급됩니다. 이를 통해 컨테이너(Container) 환경에서 키 충돌을 방지합니다.
/* include/linux/key.h — 도메인 태그 관련 구조체 */
struct key_tag {
struct rcu_head rcu;
refcount_t usage;
bool removed; /* 네임스페이스 해제됨 */
};
/* 키 인덱스 키 — 검색 시 타입+설명+태그로 매칭 */
struct keyring_index_key {
unsigned long hash;
union {
struct {
u16 desc_len;
char desc[6];
};
unsigned long x;
};
struct key_type *type;
struct key_tag *domain_tag; /* 도메인 태그 */
const char *description;
};
/* net/dns_resolver/dns_key.c — NET_DOMAIN 키 타입 예시 */
struct key_type key_type_dns_resolver = {
.name = "dns_resolver",
.flags = KEY_TYPE_NET_DOMAIN, /* 네트워크 네임스페이스별 격리 */
/* ... */
};
/* include/linux/key.h — 네임스페이스 인식 키 요청 */
struct key *request_key_net(
struct key_type *type,
const char *description,
struct net *net, /* 네트워크 네임스페이스 */
const char *callout_info)
{
/* net->key_domain이 도메인 태그로 사용됨 */
return request_key_tag(type, description,
net->key_domain,
callout_info);
}
컨테이너 격리: Docker, Kubernetes(쿠버네티스) 등의 컨테이너 환경에서 각 컨테이너가 고유한 네트워크 네임스페이스를 가지면, DNS 키도 자동으로 격리됩니다. 컨테이너 A에서 캐싱한 DNS 결과가 컨테이너 B에 영향을 주지 않으므로, 서로 다른 DNS 서버를 사용하는 컨테이너 간 안전한 격리가 보장됩니다.
사용자 정의 key_type 모듈 개발
키링 서비스의 플러그인 아키텍처를 활용하여 커널 모듈에서 사용자 정의 key_type을 구현할 수 있습니다. 새로운 키 타입은 고유한 페이로드 형식, 인스턴스화 로직(Logic), 검증 규칙을 가질 수 있습니다.
/* 사용자 정의 key_type 커널 모듈 — 완전한 예제 */
#include <linux/module.h>
#include <linux/key-type.h>
#include <linux/slab.h>
#include <keys/user-type.h>
#define MY_KEY_MAX_SIZE 4096
/* 사용자 정의 페이로드 구조체 */
struct my_key_payload {
struct rcu_head rcu;
u32 version; /* 페이로드 버전 */
u32 flags; /* 사용자 정의 플래그 */
size_t data_len;
u8 data[]; /* 가변 길이 데이터 */
};
/* 1. preparse — 인스턴스화 전 데이터 검증 및 파싱 */
static int my_key_preparse(
struct key_preparsed_payload *prep)
{
struct my_key_payload *payload;
/* 크기 제한 검사 */
if (prep->datalen == 0 || prep->datalen > MY_KEY_MAX_SIZE)
return -EINVAL;
/* 페이로드 할당 */
payload = kmalloc(sizeof(*payload) + prep->datalen,
GFP_KERNEL);
if (!payload)
return -ENOMEM;
/* 사용자 데이터 복사 및 버전 설정 */
payload->version = 1;
payload->flags = 0;
payload->data_len = prep->datalen;
memcpy(payload->data, prep->data, prep->datalen);
/* preparsed 데이터 설정 */
prep->payload.data[0] = payload;
prep->quotalen = sizeof(*payload) + prep->datalen;
return 0;
}
/* 2. free_preparse — preparse 실패 시 정리 */
static void my_key_free_preparse(
struct key_preparsed_payload *prep)
{
kfree_sensitive(prep->payload.data[0]);
}
/* 3. destroy — 키 파괴 시 페이로드 해제 */
static void my_key_destroy(struct key *key)
{
struct my_key_payload *payload;
payload = rcu_dereference_key(key);
if (payload) {
/* 민감 데이터는 kfree_sensitive로 안전 삭제 */
kfree_sensitive(payload);
}
}
/* 4. describe — /proc/keys 표시 형식 */
static void my_key_describe(
const struct key *key,
struct seq_file *m)
{
struct my_key_payload *payload;
seq_puts(m, key->description);
rcu_read_lock();
payload = rcu_dereference(key->payload.data[0]);
if (payload)
seq_printf(m, ": v%u [%zu bytes]",
payload->version, payload->data_len);
rcu_read_unlock();
}
/* 5. read — 사용자 공간으로 페이로드 읽기 */
static long my_key_read(
const struct key *key,
char *buffer, size_t buflen)
{
const struct my_key_payload *payload;
long ret;
payload = rcu_dereference_key(key);
if (!payload)
return -EKEYREVOKED;
ret = payload->data_len;
if (buffer && buflen > 0) {
if (buflen > payload->data_len)
buflen = payload->data_len;
memcpy(buffer, payload->data, buflen);
}
return ret;
}
/* 6. update — 기존 키 페이로드 갱신 */
static int my_key_update(
struct key *key,
struct key_preparsed_payload *prep)
{
struct my_key_payload *old_payload, *new_payload;
new_payload = prep->payload.data[0];
new_payload->version++; /* 버전 증가 */
/* RCU 기반 원자적 교체 */
old_payload = rcu_dereference_protected(
key->payload.data[0],
rwsem_is_locked(&key->sem));
rcu_assign_keypointer(key, new_payload);
prep->payload.data[0] = NULL; /* 이중 해제 방지 */
if (old_payload)
kfree_rcu(old_payload, rcu);
return 0;
}
/* key_type 정의 */
static struct key_type my_key_type = {
.name = "my_custom_key",
.def_datalen = 0,
.preparse = my_key_preparse,
.free_preparse = my_key_free_preparse,
.instantiate = generic_key_instantiate,
.update = my_key_update,
.destroy = my_key_destroy,
.describe = my_key_describe,
.read = my_key_read,
};
/* 모듈 초기화 / 해제 */
static int __init my_key_module_init(void)
{
return register_key_type(&my_key_type);
}
static void __exit my_key_module_exit(void)
{
/* 타입 해제 → 해당 타입의 모든 키가 KEY_FLAG_DEAD */
unregister_key_type(&my_key_type);
}
module_init(my_key_module_init);
module_exit(my_key_module_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Custom key type example");
# 사용자 정의 key_type 사용 예
# 모듈 로드
$ insmod my_key_type.ko
# 사용자 정의 타입으로 키 생성
$ keyctl add my_custom_key "config-data" "key=value;foo=bar" @s
123456789
# 키 읽기
$ keyctl print 123456789
key=value;foo=bar
# /proc/keys에서 확인
$ cat /proc/keys | grep my_custom
123456789 --alswrv 1000 1000 my_custom_key: config-data: v1 [15 bytes]
# 키 갱신
$ keyctl update 123456789 "new-config=data"
# 모듈 언로드 → 해당 타입의 모든 키 dead 처리
$ rmmod my_key_type
key_type 개발 시 주의사항:
preparse()에서 할당한 메모리는 반드시free_preparse()에서 해제해야 합니다. 인스턴스화 실패 시 free_preparse가 호출됩니다.- 민감 데이터는
kfree()대신kfree_sensitive()를 사용하여 메모리를 0으로 초기화 후 해제합니다. - 페이로드 교체 시 RCU를 사용하여 읽기 측 락(Lock) 없이 안전한 동시 접근을 보장합니다.
.read콜백을 구현하지 않으면logon타입처럼 사용자 공간에서 키 내용을 읽을 수 없습니다.
디버깅(Debugging)과 트러블슈팅
키링 서비스 관련 문제를 진단하고 해결하기 위한 기법과 도구를 정리합니다.
procfs 기반 진단
# /proc/keys — 현재 프로세스가 접근 가능한 모든 키
$ cat /proc/keys
# serial flags usage expiry perm uid gid type description
009a62c4 I--Q--- 1 perm 1f3f0000 0 0 keyring .builtin_trusted_keys: 3
009a62c5 I--Q--- 1 perm 1f0f0000 0 0 keyring .platform: 2
023ab451 I--Q--- 20200805 perm 3f010000 1000 1000 user mykey: 32
# 플래그 해석:
# I = Instantiated (인스턴스화 완료)
# R = Revoked (폐기됨)
# D = Dead (사망 — key_type 해제)
# Q = Contributes to quota (할당량 적용)
# U = Under construction (구성 중)
# N = Negative (음수 키)
# i = Invalidated (무효화됨)
# /proc/key-users — 사용자별 키 사용 통계
$ cat /proc/key-users
# UID: nkeys nkeys/quota bytes/quota
0: 42 42/1000000 8500/25000000
1000: 8 8/200 1250/20000
# 특정 키 상세 정보
$ keyctl describe 123456
# type;uid;gid;perm;description
user;1000;1000;3f010000;mykey
# 키의 보안 레이블 (SELinux 환경)
$ keyctl security 123456
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
일반적인 오류와 해결책
| 오류 코드 | 원인 | 해결책 |
|---|---|---|
-ENOKEY | 키를 찾을 수 없음 | 키 타입/설명 확인, keyctl show @s로 키링 검색. request_key.conf 핸들러 확인 |
-EKEYEXPIRED | 키가 만료됨 | keyctl timeout으로 만료 시간 연장 또는 키 갱신 |
-EKEYREVOKED | 키가 폐기됨 | 새 키 생성 필요. keyctl revoke 취소 불가 |
-EKEYREJECTED | 키 인스턴스화 거부됨 | 키 타입의 preparse 검증 실패. 데이터 형식 확인 |
-EDQUOT | 사용자 키 할당량 초과 | 불필요한 키 삭제 또는 sysctl kernel.keys.maxkeys 증가 |
-EACCES | 키 접근 권한 부족 | keyctl setperm으로 권한 조정. possessor/user/group 확인 |
-ENOMEM | 메모리 부족 | 시스템 메모리 상태 확인, big_key 사용 고려 |
-EOPNOTSUPP | 지원하지 않는 연산 | logon 타입 키의 read 시도. user 타입 사용 또는 커널 접근 필요 |
-EPERM | TPM unseal 실패 | PCR 값 변경 확인 (커널 업데이트 등). 키 재봉인 필요 |
-EINVAL | 잘못된 매개변수 | logon 키 description에 콜론(:) 포함 확인. 데이터 크기 확인 |
커널 추적(Tracing) 기반 디버깅
# ftrace로 키 관련 함수 추적
$ echo 1 > /sys/kernel/debug/tracing/events/key/enable
# 키 이벤트 카테고리별 활성화
$ echo 1 > /sys/kernel/debug/tracing/events/key/key_alloc/enable
$ echo 1 > /sys/kernel/debug/tracing/events/key/key_free/enable
$ echo 1 > /sys/kernel/debug/tracing/events/key/key_request/enable
# 추적 결과 확인
$ cat /sys/kernel/debug/tracing/trace
# ... key_alloc: serial=123456 type=user desc="my-key"
# ... key_request: type=user desc="my-key" flags=0x0
# 추적 비활성화
$ echo 0 > /sys/kernel/debug/tracing/events/key/enable
# request_key upcall 디버깅
# /etc/request-key.conf에 디버그 핸들러 추가
create * * * /usr/share/keyutils/request-key-debug.sh %k %d %c %S
# 디버그 스크립트 출력 확인
$ journalctl -t request-key-debug
# key=123456 op=create type=user desc="test:key" ring=session
# strace로 키 시스템 콜 추적
$ strace -e add_key,request_key,keyctl -f keyctl add user test "data" @s
add_key("user", "test", "data", 4, -3) = 123456789
키 메모리 보안 검증
# 키 데이터가 스왑에 노출되지 않는지 확인
# 1. 스왑 비활성화 확인 (보안 환경)
$ swapon --show
# 출력 없으면 스왑 비활성 → 안전
# 2. big_key가 shmem 암호화를 사용하는지 확인
$ grep BIG_KEYS /boot/config-$(uname -r)
CONFIG_BIG_KEYS=y
# 3. 키 메모리 잠금 확인 (mlock)
# trusted/encrypted 키는 커널 공간에만 존재 → 스왑 불가
# user 키의 페이로드는 kmalloc으로 할당 → 비스왑 가능 영역
# 4. 키 삭제 후 메모리 제로화 확인
# kfree_sensitive() 사용 여부는 key_type의 destroy 콜백에 의존
# trusted/encrypted 키: 반드시 kfree_sensitive 사용
# user 키: kfree 사용 (일반 free — 보안 수준 낮음)
# 5. /dev/kmem으로 키 데이터 접근 차단 확인
$ cat /proc/sys/kernel/kptr_restrict
1 # 커널 포인터 은닉 활성 → 키 메모리 주소 비공개
# 6. Lockdown LSM으로 키 보호 강화
$ cat /sys/kernel/security/lockdown
integrity # 또는 confidentiality → 커널 메모리 읽기 차단
트러블슈팅 체크리스트
| 증상 | 점검 항목 | 명령어 |
|---|---|---|
| 키 추가 실패 | 할당량 확인 | cat /proc/key-users |
| 키 추가 실패 | 커널 설정 확인 | grep CONFIG_KEYS /boot/config-$(uname -r) |
| 키 검색 실패 | 키링 계층 확인 | keyctl show @s, keyctl show @u |
| 키 검색 실패 | 키 만료/폐기 여부 | keyctl describe <serial> |
| request_key upcall 실패 | 핸들러 설정 확인 | cat /etc/request-key.conf |
| request_key upcall 실패 | 핸들러 실행 권한 | ls -la /sbin/request-key |
| trusted 키 unseal 실패 | TPM 상태 확인 | tpm2_pcrread sha256:0,7 |
| trusted 키 unseal 실패 | PCR 값 변경 여부 | tpm2_pcrread과 봉인 시 값 비교 |
| encrypted 키 load 실패 | 마스터 키 존재 확인 | keyctl search @s user master-key |
| 키링 제한 거부 | 서명 검증 실패 | openssl verify로 인증서 체인 확인 |
| 권한 거부 | SELinux 정책 확인 | ausearch -m AVC -ts recent | grep key |
| 성능 저하 | 키 수 과다 | keyctl show @s의 키 수 확인, 불필요한 키 정리 |
일반적인 실수:
logon키의 description에 콜론(:)을 빠뜨리면-EINVAL이 발생합니다. 반드시"서비스:설명"형식을 사용하세요.keyctl revoke로 폐기한 키는 복원할 수 없습니다. 테스트 시keyctl invalidate(즉시 GC 대상)를 먼저 고려하세요.- 세션 키링에 추가한 키는
su나sudo로 전환 시 접근할 수 없을 수 있습니다. 세션 키링이 변경되기 때문입니다. - 컨테이너 환경에서 호스트(Host)와 컨테이너의 키 네임스페이스가 다를 수 있습니다.
CONFIG_USER_NS설정을 확인하세요.
최신 동향 (2025-2026)
키링 서브시스템의 핵심 자료 구조(struct key, keyring)는 2010년대 중반부터 크게 바뀌지 않았지만,
어떤 키가 이 체계에 들어올 수 있는가를 정하는 정책은 2022년 .machine 키링 도입 이후 꾸준히 진화하고 있습니다.
2025-2026 시점의 변화는 포스트 양자 전환기의 모듈 서명, TPM2 봉인 키(Sealed Key)의 커널 매커니즘 정착, 배포판 crypto-policies와의 결합 세 갈래로 요약됩니다.
| 시점 | 변경 | 실무 영향 |
|---|---|---|
| Linux 5.19 (2022) | .machine 키링 도입 | shim의 MokListTrusted 변수가 설정될 때 MOK이 자동으로 .machine으로 옮겨가 .secondary_trusted_keys와 함께 링크됩니다. |
| Linux 6.2 (2023) | CA-enforced MOK 엔롤먼트 | CONFIG_INTEGRITY_CA_MACHINE_KEYRING 활성 시 MOK 추가에 CA 서명 체인이 요구됩니다. 개발자 키 직접 등록 경로가 좁아집니다. |
| Linux 6.7 (2024) | ACL(Access Control List) 기반 키 권한 | 기존 16비트 권한 대신 ACL 방식으로 더 세밀한 키 접근 제어가 가능해졌으며, SELinux 정책과 중첩할 수 있습니다. |
| Linux 6.11 (2024-09) | getrandom() vDSO 정착 | 키 생성 시 대량 난수 소비 경로가 시스템 콜(System Call) 경계를 타지 않아, 컨테이너 초기 대량 키 생성(TLS 핸드셰이크 storm 등) 지연이 줄었습니다. |
| Linux 6.15 (2025-04) | TPM2 trusted key 정책 옵션 확장 | PCR 바인딩 외에 policyauthorize, authvalue를 더 유연하게 조합해 봉인 키를 다룰 수 있고, Auth 실패 시 재시도 정책이 개선되었습니다. |
| Linux 6.19 (2025-2026 예정) | ML-DSA(Dilithium) 기반 모듈 서명 패치(Patch) 시리즈 | 커널이 처음으로 포스트 양자 서명을 검증 체인에 받는 시점입니다. CONFIG_MODULE_SIG_KEY 알고리즘 선택지에 ML-DSA 변종이 추가됩니다. |
.machine 키링과 MOK 전파 경로
UEFI Secure Boot 체인에서 키링으로 진입하는 경로가 세 갈래로 정리되었습니다. .platform은 펌웨어가 넣고, .builtin_trusted_keys는 커널 이미지 자체가 가지며, .machine은 shim이 MOK을 승격시킨 결과입니다.
# UEFI / shim / 커널 키링 상태를 한 번에 점검
mokutil --sb-state
mokutil --list-enrolled | head
mokutil --trust-mok # MokListTrusted 상태 확인
# 커널이 실제로 들고 있는 시스템 키링
keyctl list %:.platform
keyctl list %:.builtin_trusted_keys
keyctl list %:.secondary_trusted_keys
keyctl list %:.machine 2>/dev/null || echo "MOK이 .machine으로 승격되지 않았습니다."
# CA-enforced MOK(6.2+) 설정 여부
scripts/config --state INTEGRITY_CA_MACHINE_KEYRING
scripts/config --state INTEGRITY_CA_MACHINE_KEYRING_MAX
TPM2 trusted keys — 2025 기준 실전 워크플로
trusted 키 타입은 TPM이 봉인(Seal)한 대칭 키를 커널이 안전하게 받아 다른 키(encrypted, fscrypt, dm-crypt)로 파생시키는 핵심 경로입니다.
2024-2025 커널 업데이트는 정책 옵션을 더 다양하게 받을 수 있도록 했습니다.
# TPM PCR 0,7에 바인딩한 봉인 키 생성 (Secure Boot 상태가 바뀌면 Unseal 실패)
keyctl add trusted kmk \
"new 32 keyhandle=0x81000001 pcrinfo=sha256:0,7" @u
# policyauthorize로 서명된 정책 문서만 허용 (6.15 확장)
keyctl add trusted kmk-policy \
"new 32 keyhandle=0x81000001 \
policydigest=<SHA256> \
policyauthorize=policy.key" @u
# 봉인 키 blob을 파일로 내보내 재부팅 후에도 복구 가능
keyctl pipe $(keyctl search @u trusted kmk) > kmk.blob
# 다른 부팅에서 재로드
keyctl add trusted kmk "load $(cat kmk.blob)" @u
# 봉인된 kmk에서 파생한 encrypted 키 (LUKS, fscrypt, DRBG 시드 공용)
keyctl add encrypted evmkey \
"new trusted:kmk 64" @u
systemd-pcrphase 단계별 잠금(Lock)이 가능합니다. PCR 0은 UEFI 펌웨어(Firmware) 변동을 포착하며, 벤더 펌웨어 업데이트 후 재봉인이 필요합니다. 이 세 개를 조합하는 방식이 2025년 Ubuntu, Fedora가 기본으로 택하는 구성입니다.
모듈 서명의 포스트 양자 로드맵
커널 모듈 서명은 지금까지 RSA(기본 4096bit) 또는 ECDSA(P-256/P-384) 기반이었습니다. 2025-2026 사이에 LKML에서 논의되는 패치 시리즈는 ML-DSA-65 / ML-DSA-87을 선택 가능하게 추가하는 방향으로 정리되고 있습니다.
-
서명 파일 크기 — ECDSA-P384 서명이 96바이트인 반면 ML-DSA-65 서명은 3,293바이트, ML-DSA-87은 4,627바이트입니다.
modinfo가 보는 서명 블록이 커지므로CONFIG_MODULE_SIG_FORMAT을 손볼 여지가 있습니다. -
빌드 파이프라인(Pipeline) — 배포판이 공개키를
.builtin_trusted_keys에 내장해야 하므로, vmlinux가 커지고 PIC 커널 이미지 크기 계획이 재조정됩니다. -
검증 성능 — 현재 x86-64 단일 코어에서 ML-DSA-65 검증은 수
us수준으로 ECDSA보다 약간 느리지만, 초기 모듈 로드가 많은 Android, 컨테이너 이미지 부팅에는 체감 가능합니다. -
FIPS 경계 — GKI FIPS 140 모듈, RHEL FIPS 정책은 기존 알고리즘과 PQC 알고리즘을 나란히 지원해야 하므로,
module_sig_check의 정책 매트릭스가 확대됩니다.
# 현재 커널 설정에서 모듈 서명 알고리즘 확인
scripts/config --state MODULE_SIG_KEY
scripts/config --state MODULE_SIG_HASH
scripts/config --state MODULE_SIG_SHA512
# 커널 빌드 시 X.509 서명 키 재생성 예시 (현재 기본 RSA)
openssl req -new -nodes -utf8 -sha512 -days 36500 \
-batch -x509 -config certs/default_x509.genkey \
-outform PEM -out signing_key.pem -keyout signing_key.pem
# 6.19 이후 예상 명령 (ML-DSA-65 서명 키) — 실제 심볼은 변동 가능
# openssl genpkey -algorithm ML-DSA-65 -out signing_key_mldsa.pem
# 모듈 서명 검증 경로 커널 로그
dmesg | grep -E "module.*signature|key.*verification"
배포판 crypto-policies와의 맞물림
RHEL 10, Fedora 42+, Debian trixie는 /etc/crypto-policies가 OpenSSL, GnuTLS, OpenSSH, OpenJDK뿐 아니라 PAM과 일부 키 저장소까지 묶어 관리합니다.
키링 서브시스템 자체가 정책을 해석하지는 않지만, asymmetric 키 검증과 IMA 서명 체인이 keyctl pkey_verify로 내려올 때 해시/서명 알고리즘이 시스템 전역 정책과 충돌하면 즉시 -EKEYREJECTED가 발생합니다.
# 시스템 전역 정책 점검
update-crypto-policies --show
cat /etc/crypto-policies/back-ends/opensslcnf.config | grep -E "SignatureAlg|Group"
# IMA 로그에서 키 검증 실패 사례 추적
ausearch -m INTEGRITY_DATA -ts recent | grep -i rejected
journalctl -k | grep -E "ima.*invalid_signature|key.*not permitted"
# 시스템 키링에 등록된 asymmetric 키의 해시/서명 조합이 정책과 호환되는지
keyctl list %:.builtin_trusted_keys | awk '{print $1}' | \
xargs -I{} keyctl describe {} 2>/dev/null | grep -E "asymmetric|sha"
관련 문서
외부 참고 자료
- Kernel Key/Keyring Service — Core Documentation — 커널 키/키링 서비스 핵심 공식 문서입니다
- Trusted and Encrypted Keys — TPM 봉인(Sealed) 키 및 암호화 키 공식 문서입니다
- Kernel Keys — Documentation Index — 커널 키 관리 문서 전체 목록입니다
- keyctl(2) — Linux man page — keyctl 시스템 콜 매뉴얼 페이지(Page)입니다
- add_key(2) — Linux man page — 키 추가 시스템 콜 매뉴얼입니다
- request_key(2) — Linux man page — 키 요청 시스템 콜 매뉴얼입니다
- keyrings(7) — Linux man page — 키링 개요 및 사용법 매뉴얼입니다
- Linux 커널 security/keys/ 소스 코드 — 키링 서브시스템 커널 소스입니다
- keyctl(1) — Linux man page — keyctl 사용자 공간 도구 매뉴얼입니다
- Kernel Crypto API — 키 타입에서 사용하는 커널 암호화 프레임워크 문서입니다