키링(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 알림까지 커널 소스 기반으로 심층 분석합니다.

전제 조건: 커널 보안 개요암호화 서브시스템의 기본 개념을 먼저 이해하세요. TPM 관련 내용은 Secure Boot 문서도 함께 참고하면 좋습니다.
일상 비유: 키링은 열쇠 보관함(key cabinet)과 같습니다. 각 열쇠(key)는 이름표가 붙어 있고, 보관함(keyring)은 중첩될 수 있습니다. 열쇠마다 유형이 다르고(자동차 키, 사무실 키, 금고 키), 누가 사용할 수 있는지 접근 권한이 지정됩니다. TPM sealed key는 금고 안에 잠긴 열쇠로, 금고(TPM)가 없으면 꺼낼 수 없습니다.

핵심 요약

  • 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()이 키 등록·조회·조작의 유일한 진입점입니다.

단계별 이해

  1. 키와 키링 구분
    개별 키와 그것을 담는 키링 컨테이너(Container)를 먼저 구분합니다.
  2. 자료구조 확인
    struct key, key_type, 권한 모델이 어떻게 연결되는지 봅니다.
  3. 시스템 콜 흐름 이해
    add_key(), request_key(), keyctl()가 어떤 상황에서 쓰이는지 정리합니다.
  4. 실사용 시나리오 연결
    fscrypt, dm-crypt, 커널 모듈 서명처럼 실제 서브시스템이 키링을 어떻게 활용하는지 확인합니다.
  5. 수명주기와 보안 검토
    만료, quota, request-key upcall, 플랫폼 키링까지 운영 관점에서 정리합니다.

Key Retention Service 아키텍처

Key Retention Service는 Linux 2.6.10에서 David Howells에 의해 도입되었습니다. 핵심 목적은 암호화 키와 인증 토큰을 커널 공간에 안전하게 보관하고, 프로세스 간 공유를 제어하며, 키의 생명주기를 자동 관리하는 것입니다. 소스 코드는 security/keys/ 디렉토리에 위치합니다.

사용자 공간 커널 공간 keyctl 유틸리티 libkeyutils /sbin/request-key fscryptctl cryptsetup add_key() / request_key() / keyctl() key_type 레지스트리 user, logon, encrypted... 키링 계층 session/process/thread 접근 제어 permissions / ACL GC / 만료 key_gc_work fscrypt dm-crypt 모듈 서명 검증 IMA/EVM TLS handshake TPM 서브시스템 seal / unseal / PCR

키링 서브시스템의 주요 구성 요소는 다음과 같습니다.

구성 요소소스 위치역할
key.csecurity/keys/key.cstruct key 생성, 검색, 삭제, 참조 카운트 관리
keyring.csecurity/keys/keyring.c키링(키 컬렉션) 관리, 연관 배열 기반 검색
keyctl.csecurity/keys/keyctl.ckeyctl() 시스템 콜 구현
request_key.csecurity/keys/request_key.crequest_key upcall 메커니즘
gc.csecurity/keys/gc.c만료/폐기 키의 가비지 컬렉션
permission.csecurity/keys/permission.c키 접근 권한 검사
encrypted-keys/security/keys/encrypted-keys/encrypted 키 타입 구현
trusted-keys/security/keys/trusted-keys/TPM sealed trusted 키 타입 구현
big_key.csecurity/keys/big_key.cshmem 기반 대용량 키 저장

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;      /* 키링의 경우: 하위 키 목록 */
        };
    };
};

주요 필드를 분석합니다.

필드타입설명
usagerefcount_t참조 카운트. key_get()/key_put()으로 증감. 0이 되면 GC 대상
serialkey_serial_t커널 전역 고유 일련번호. 사용자 공간에서 키를 식별하는 핸들
semrw_semaphore키 페이로드 읽기/쓰기 동기화. read는 키 사용, write는 키 갱신
permkey_perm_t30비트 접근 권한. possessor/user/group/other 각 7비트 + 2비트 예약
flagsunsigned longKEY_FLAG_DEAD, KEY_FLAG_REVOKED, KEY_FLAG_IN_QUOTA 등 상태 플래그
stateshort양수: 인스턴스화 완료, 0: 대기 중, 음수: 오류 코드
payloadunion key_payload실제 키 데이터. key_type의 콜백(Callback) 함수가 해석
typestruct 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 instantiate / describe / destroy user logon encrypted trusted big_key asymmetric 사용자 공간 read 일반 blob 저장 가장 기본적 타입 read 차단 커널 전용 접근 dm-crypt 키 등 마스터 키로 암호화 user/trusted 래핑 AES-256-GCM TPM seal/unseal PCR 바인딩 하드웨어 보호 대용량 키 shmem/tmpfs 저장 AES 암호화 보관 X.509 인증서 PKCS#7 서명 검증 모듈/IMA 검증 keyring (특수 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 키 생성(new) 흐름 사용자 공간 add_key("encrypted", "new user:master 64") encrypted_init() 포맷/마스터/크기 파싱 get_random_bytes() 64바이트 평문 키 생성 request_key(master) 마스터 키 검색 user:master 또는 trusted:master AES-256-GCM 래핑 암호화 encrypted 키 페이로드 구조 (디스크 저장 형태) format "default" master_desc "user:master" datalen "64" IV 12바이트 난수 encrypted_data AES-GCM(평문 키) + 16B 태그 HMAC 무결성 검증 encrypted 키 복원(load) 흐름 디스크에서 blob 로드 "load <hex blob>" datablob_parse() blob 구조 파싱 request_key(master) 마스터 키 검색 AES-256-GCM 복호화 + 태그 검증 평문 키 복원 커널 메모리에만 존재 보안 특성 평문 키: 커널 메모리에만 존재 디스크 저장: 암호문 + IV + 태그만 마스터 키 없이 복원 불가

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_SIZEkmalloc (직접 메모리)없음커널 직접 매핑(Mapping)
> PAGE_SIZE ~ 1 MiBshmem/tmpfs 파일AES-GCM (커널 RNG IV)페이지 캐시(Page Cache) (스왑(Swap) 가능)

asymmetric 키 타입 (X.509 / PKCS#7)

asymmetric 키 타입은 X.509 인증서, PKCS#7 서명, 공개키/개인키 쌍을 저장합니다. 커널 모듈 서명 검증, IMA/EVM 서명 검증, kexec 서명 검증 등에서 사용됩니다.

PKCS#7 서명 모듈/IMA/kexec pkcs7_verify() 서명 검증 + 인증서 추출 x509_validate_trust() 체인 상의 인증서 검증 .builtin_trusted_keys 커널 빌트인 신뢰 앵커 .secondary_trusted_keys .machine .platform 검증 성공 검증 실패 → -EKEYREJECTED
/* 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);
};

지원되는 비대칭 키 알고리즘:

알고리즘커널 설정용도
RSACONFIG_CRYPTO_RSA모듈 서명, X.509, PKCS#7
ECDSA (P-256, P-384)CONFIG_CRYPTO_ECDSAIMA 서명, 경량 인증서
EdDSA (Ed25519)CONFIG_CRYPTO_ECRDSAGOST 표준 서명
SM2CONFIG_CRYPTO_SM2중국 국가 표준 (GB/T 32918)

키링 계층 구조

Linux는 프로세스별, 사용자별로 키링을 계층적으로 관리합니다. 키를 검색할 때 thread → process → session 순으로 탐색하며, 각 키링은 struct key의 특수 인스턴스입니다(key_type은 "keyring").

프로세스별 키링 계층 Thread Keyring Process Keyring Session Keyring 스레드 전용, clone 시 비공유 TGID 공유, 같은 프로세스의 모든 스레드 세션 공유, fork 시 상속, PAM 관리 사용자별 키링 User Keyring User Session Keyring UID별, 모든 세션에서 공유 검색 순서 1 2 3 4 키링 내 키 예시 Session Keyring (@s): user: api-token encrypted: vol-key keyring (중첩) trusted: tpm-key logon: nfs4-key
키링특수 ID수명공유 범위생성 시점
ThreadKEY_SPEC_THREAD_KEYRING (-1)스레드(Thread) 종료 시 파괴해당 스레드만첫 접근 시 지연(Latency) 생성
ProcessKEY_SPEC_PROCESS_KEYRING (-2)프로세스 종료 시 파괴같은 TGID의 모든 스레드첫 접근 시 지연 생성
SessionKEY_SPEC_SESSION_KEYRING (-3)세션 종료 시 파괴세션의 모든 프로세스PAM 또는 keyctl session
UserKEY_SPEC_USER_KEYRING (-4)UID 네임스페이스(Namespace) 수명같은 UID의 모든 프로세스UID 첫 사용 시
User SessionKEY_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_ID0특수 키링 ID를 실제 serial로 변환
KEYCTL_JOIN_SESSION_KEYRING1새 세션 키링 생성 또는 기존 키링에 조인
KEYCTL_UPDATE2키 페이로드 갱신
KEYCTL_REVOKE3키 폐기 (사용 불가 처리)
KEYCTL_DESCRIBE6키 메타데이터 조회 (타입, UID, GID, perm)
KEYCTL_CLEAR7키링의 모든 키 제거
KEYCTL_LINK8키를 키링에 링크
KEYCTL_UNLINK9키를 키링에서 언링크
KEYCTL_SEARCH10키링 재귀 검색
KEYCTL_READ11키 페이로드 읽기
KEYCTL_SET_TIMEOUT15키 만료 시간 설정 (초 단위)
KEYCTL_SET_PERM5키 접근 권한 설정
KEYCTL_INVALIDATE21키 즉시 무효화(Invalidation) (GC 대기 없이)
KEYCTL_RESTRICT_KEYRING29키링에 추가 가능한 키 제한
KEYCTL_WATCH_KEY32키 변경 이벤트 감시 (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)의 인증 토큰 획득에 주로 사용됩니다.

1. request_key() 커널/사용자 공간 호출 2. 키링 검색 thread → process → session 발견 → 키 반환 미발견 → upcall 3. 미완성 키 생성 KEY_FLAG_USER_CONSTRUCT 4. /sbin/request-key 실행 call_usermodehelper() 5. request-key.conf 파싱 /etc/request-key.conf 6. 핸들러 실행 키 획득 프로그램 7. keyctl instantiate 키 페이로드 설정 → 완성 8. 호출자 깨우기 대기 중인 request_key() 완료 /etc/request-key.conf 예: create user nfs4:* * /usr/sbin/nfs4_id_to_key %k %d %c %S create user dns_resolver:* * /usr/sbin/key.dns_resolver %k

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)가 만료, 폐기, 무효화된 키를 자동으로 정리합니다.

미완성 UNINSTANTIATED 활성 INSTANTIATED 갱신됨 UPDATED 만료 EXPIRED 폐기 REVOKED 음수 NEGATIVE 파괴 key_gc_work instantiate update expiry keyctl_revoke negate (upcall 실패) refcount=0 가비지 컬렉터 (key_gc_work) - 만료된 키 수거 (key_schedule_gc) - dead/revoked 키 정리 - 주기적 워크큐 기반 실행
/* 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/maxkeys200일반 사용자의 최대 키 개수
kernel/keys/maxbytes20000일반 사용자의 최대 키 바이트
kernel/keys/root_maxkeys1000000root의 최대 키 개수
kernel/keys/root_maxbytes25000000root의 최대 키 바이트
kernel/keys/gc_delay300GC 실행 지연 시간 (초)
⚠️

할당량 초과: 키 생성 시 할당량을 초과하면 -EDQUOT을 반환합니다. 대량의 키를 다루는 서비스(NFS4 등)에서는 KEY_FLAG_IN_QUOTA 플래그를 해제하여 할당량에서 제외하거나, sysctl로 제한을 늘려야 합니다.

키 접근 권한

키 접근 권한은 파일 시스템의 rwx와 유사하지만, 4개의 카테고리에 각 7개의 권한 비트를 사용합니다. 총 30비트(key_perm_t)로 구성됩니다.

비트권한설명
bit 0VIEW키 속성 조회 (describe)
bit 1READ키 페이로드 읽기
bit 2WRITE키 페이로드 갱신, 키링에 링크/언링크
bit 3SEARCH키링 검색 시 이 키 발견 가능
bit 4LINK키를 다른 키링에 링크 가능
bit 5SETATTR속성 변경 (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 커널 빌트인 인증서 (vmlinux에 내장) .secondary_trusted_keys 런타임 추가 인증서 (제한적) .machine 머신 소유자 인증서 (MOK) .platform UEFI db/dbx 인증서 .blacklist 해시 블랙리스트 (UEFI dbx) .ima IMA 정책 서명 검증 키 서명 검증 검증 용도 모듈 서명 / IMA / kexec / PKCS#7
키링커널 설정내용수정 가능 여부
.builtin_trusted_keys(기본 내장)커널 빌드 시 내장된 X.509 인증서불가 (읽기 전용(Read-Only))
.secondary_trusted_keysCONFIG_SECONDARY_TRUSTED_KEYRING빌트인 키로 서명된 인증서 추가 가능제한적 추가
.machineCONFIG_INTEGRITY_MACHINE_KEYRINGMOK(Machine Owner Key) 인증서UEFI MOK 관리
.platformCONFIG_INTEGRITY_PLATFORM_KEYRINGUEFI Secure Boot db 인증서UEFI 설정에서 관리
.blacklistCONFIG_SYSTEM_BLACKLIST_KEYRING거부할 인증서/모듈 해시(Hash)제한적 추가
.imaCONFIG_IMA_KEYRINGS_PERMIT_SIGNED_BY_BUILTIN_OR_SECONDARYIMA 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를 통해 사용자 공간에 비동기로 전달합니다. 키의 생성, 갱신, 폐기, 무효화, 만료 이벤트를 감시할 수 있습니다.

키 이벤트 발생 update/revoke/expire... post_one_notification() 알림 메시지 생성 watch_queue 링 버퍼 pipe 내부 페이지 pipe fd read()/poll() 사용자 이벤트 처리 키 알림 이벤트 (key_notification) NOTIFY_KEY_INSTANTIATED 키 인스턴스화 완료 NOTIFY_KEY_UPDATED 페이로드 갱신 NOTIFY_KEY_REVOKED 키 폐기 NOTIFY_KEY_INVALIDATED 키 무효화 NOTIFY_KEY_LINKED NOTIFY_KEY_UNLINKED NOTIFY_KEY_CLEARED
/* 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_KEYSYKey Retention Service 핵심 (필수)
CONFIG_KEYS_REQUEST_CACHENrequest_key 결과 태스크별 캐시(Cache)
CONFIG_PERSISTENT_KEYRINGSN영구 키링 (UID 네임스페이스 독립)
CONFIG_TRUSTED_KEYSMTPM/TEE sealed 키 타입
CONFIG_TRUSTED_KEYS_TPMYTPM2 기반 trusted 키
CONFIG_TRUSTED_KEYS_TEENARM TrustZone TEE 기반 trusted 키
CONFIG_TRUSTED_KEYS_CAAMNNXP CAAM 기반 trusted 키
CONFIG_ENCRYPTED_KEYSM암호화된 키 타입
CONFIG_BIG_KEYSY대용량 키 타입 (shmem 기반)
CONFIG_KEY_DH_OPERATIONSNDiffie-Hellman 키 교환
CONFIG_KEY_NOTIFICATIONSNwatch_queue 키 알림
CONFIG_ASYMMETRIC_KEY_TYPEY비대칭 키 타입 (X.509/PKCS#7)
CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPEY공개키 하위 타입
CONFIG_X509_CERTIFICATE_PARSERYX.509 인증서 파서
CONFIG_PKCS7_MESSAGE_PARSERYPKCS#7 메시지 파서
CONFIG_SYSTEM_TRUSTED_KEYRINGY.builtin_trusted_keys 키링
CONFIG_SECONDARY_TRUSTED_KEYRINGN.secondary_trusted_keys 키링
CONFIG_SYSTEM_BLACKLIST_KEYRINGN.blacklist 키링
CONFIG_INTEGRITY_PLATFORM_KEYRINGNUEFI .platform 키링
CONFIG_INTEGRITY_MACHINE_KEYRINGNMOK .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/ 디렉토리의 주요 흐름을 추적합니다.

register_key_type() 흐름 모듈 init module_init() register_key_type() security/keys/key.c 중복 이름 체크 key_types_sem write lock key_types 리스트 추가 list_add(&type->link) add_key() 시스템 콜 내부 SYSCALL_DEFINE5 key_type 검색 type->preparse() key_alloc() key_instantiate_and_link() 핵심 내부 자료구조 key_serial_tree (RB 트리) — serial로 키 검색 key_user_tree (RB 트리) — UID별 할당량 추적 assoc_array — 키링 내 키 저장 (4원 트라이)

키링 내부의 연관 배열(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) 기반으로 동작하며, 키 상태 변경 시 스케줄링됩니다.

GC 트리거 소스 key_schedule_gc() 키 만료 타이머 만료 keyctl_invalidate() unregister_key_type() key_gc_work system_wq 워크큐 gc_delay(300초) 후 실행 1단계: 스캔 key_serial_tree 순회 2단계: 분류 dead/revoked/expired 분류 3단계: 정리 키링에서 언링크 + 해제 정리 대상 키 조건 KEY_FLAG_DEAD 설정 → key_type이 해제된 키 KEY_FLAG_REVOKED 설정 → 명시적으로 폐기된 키 expiry < 현재 시각 → 만료 시간이 지난 키 KEY_FLAG_INVALIDATED 설정 GC 보호 메커니즘 KEY_FLAG_KEEP 영구 유지 (플랫폼 키링 등) KEY_FLAG_BUILTIN 커널 빌트인 키 (부팅 시 생성) refcount > 0 사용 중인 키 보호 → 위 조건 중 하나라도 해당하면 GC 대상에서 제외
/* 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_QUOTA0x0000할당량 적용 (기본)
KEY_ALLOC_NOT_IN_QUOTA0x0002할당량 면제 (커널 내부 키)
KEY_ALLOC_BUILT_IN0x0004빌트인 키 (부팅 시 생성)
KEY_ALLOC_BYPASS_RESTRICTION0x0008링크 제한 우회
KEY_ALLOC_UID_KEYRING0x0010UID 키링 생성
KEY_ALLOC_SET_KEEP0x0020GC 보호 설정

할당된 키에 페이로드를 설정하고 대상 키링에 링크합니다. 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을 수행하고, 결과를 대상 키링에 자동 링크합니다.

/* 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;
}
네트워크 서비스키 타입핸들러용도
NFS4user (id_resolver)nfsidmapUID/GID → 이름 매핑
NFS4 KerberosrxrpcgssproxyKerberos TGT 캐싱
CIFS/SMBlogon (cifs)cifscredsSMB 비밀번호 캐싱
AFSrxrpcafs_settokenAFS 토큰 관리
DNSdns_resolverkey.dns_resolver커널 DNS 해석
kTLSasymmetric / usertlshdTLS 세션 키/인증서

TPM 연동

TPM(Trusted Platform Module)과 키링의 연동은 키 보호의 최고 수준을 제공합니다. trusted 키 타입의 seal/unseal 흐름, PCR 바인딩, 정책 기반 봉인을 상세히 분석합니다.

TPM 2.0 하드웨어 SRK (저장 루트 키) PCR 레지스터 EA 정책 엔진 RNG 엔진 HMAC / 대칭 엔진 커널 키링 계층 trusted 키 TPM seal/unseal 보호 encrypted 키 trusted 키로 래핑 dm-crypt fscrypt IMA/EVM ecryptfs seal / unseal PCR 바인딩 정책 PCR[0-7] = UEFI/GRUB/커널/initramfs 측정값 PCR 값이 변경되면 unseal 실패 → 부팅 환경 변조 탐지 PolicyPCR + PolicyPassword → 이중 인증 가능 디스크 저장 sealed blob만 저장 (평문 없음)

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 블록 암호화]

공격자가 디스크만 탈취한 경우:

# 전체 키 체인 설정 예시

# 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-usersUID별 키 수/할당량
세션 키링 트리keyctl show @s계층적 키링 구조
빌트인 신뢰 키keyctl show %:.builtin_trusted_keysX.509 인증서 목록
TPM 키 지원grep TRUSTED_KEYS /boot/config-$(uname -r)CONFIG_TRUSTED_KEYS=m
할당량 설정sysctl kernel.keys.maxkeys200 (기본)
keyutils 설치keyctl --version버전 출력

Diffie-Hellman 키 교환

Linux 4.7부터 키링 서비스는 커널 공간에서 Diffie-Hellman(DH) 키 교환을 지원합니다(CONFIG_KEY_DH_OPERATIONS). 사용자 공간의 비밀 키를 노출하지 않고 공유 비밀을 안전하게 계산할 수 있습니다. NFS4 Kerberos와 같은 프로토콜(Protocol)에서 세션 키 유도에 활용됩니다.

Alice (로컬) 비밀 키 a (키링 저장) 공개 값 g^a mod p prime p, generator g Bob (원격) 비밀 키 b (키링 저장) 공개 값 g^b mod p g^a mod p 전송 g^b mod p 전송 KEYCTL_DH_COMPUTE (커널 공간) 공유 비밀 = (g^b)^a mod p = g^(ab) mod p 비밀 키 a + 공개 값 g^b KDF (HKDF-SHA256) 공유 비밀 → 세션 키 유도
/* 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(&params, _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 매개변수타입설명
privatekey_serial_t비밀 키 serial (지수(Exponent))
primekey_serial_t소수 p serial (모듈러스)
basekey_serial_t밑 값 serial (generator 또는 상대 공개값)
kdf.hashnamechar *KDF 해시 알고리즘 ("sha256", "sha512")
kdf.otherinfolen__u32KDF 추가 정보(OtherInfo) 길이

영구 키링 (Persistent Keyrings)

Linux 3.13에서 도입된 영구 키링(CONFIG_PERSISTENT_KEYRINGS)은 사용자의 세션 종료 후에도 유지되는 키링입니다. 일반 세션 키링은 마지막 세션이 종료되면 파괴되지만, 영구 키링은 별도의 만료 타이머(Timer)에 의해 관리되어 재로그인 시에도 키를 유지할 수 있습니다.

세션 1 (SSH 로그인) thread keyring process keyring session keyring (@s) 세션 2 (콘솔 로그인) thread keyring process keyring session keyring (@s) 세션 종료 → 파괴 세션 종료 → 파괴 영구 키링 (persistent) UID별 영구 키링 NFS4 토큰 Kerberos TGT 세션 종료 후에도 유지 (타이머 기반) KEYCTL_GET_PERSISTENT로 연결 키링 수명 비교 Thread Keyring 스레드 종료 시 파괴 Session Keyring 마지막 세션 종료 시 파괴 User Keyring UID 네임스페이스 수명 Persistent Keyring 타이머 만료까지 유지 (기본 3일) 접근 시 타이머 리셋 짧음
/* 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_expiry259200 (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_trustedrestrict_link_by_builtin_trusted().builtin_trusted_keys로 서명 검증된 키만 허용
builtin_and_secondaryrestrict_link_by_builtin_and_secondary_trusted()빌트인 또는 이미 등록된 secondary 키로 서명 검증된 키 허용
key_or_keyringrestrict_link_by_key_or_keyring()지정된 키 또는 키링 내 키로 서명 검증된 키만 허용
key_or_keyring_chainrestrict_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) 환경에서 키 충돌을 방지합니다.

도메인 태그 없이 (충돌 위험) dns_resolver: *.example.com dns_resolver: *.example.com 컨테이너 A (netns-1) 컨테이너 B (netns-2) 같은 키를 공유 → 잘못된 DNS 결과 참조 도메인 태그 적용 (격리) dns: *.example.com [tag=netns-1] dns: *.example.com [tag=netns-2] 컨테이너 A → 192.168.1.10 컨테이너 B → 10.0.0.20 별개의 키로 격리 → 정확한 결과 도메인 태그 구현 상세 keyring_index_key type + description + domain_tag → 3개 모두 일치해야 같은 키 KEY_TYPE_NET_DOMAIN key_type.flags에 설정 → 네트워크 네임스페이스별 격리 적용 대상 타입 dns_resolver, rxrpc cifs, afs, nfs4 도메인 태그는 key_type 등록 시 KEY_TYPE_NET_DOMAIN 플래그로 자동 활성화 → request_key_net()에서 현재 netns를 태그로 사용
/* 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 타입 사용 또는 커널 접근 필요
-EPERMTPM 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 대상)를 먼저 고려하세요.
  • 세션 키링에 추가한 키는 susudo로 전환 시 접근할 수 없을 수 있습니다. 세션 키링이 변경되기 때문입니다.
  • 컨테이너 환경에서 호스트(Host)와 컨테이너의 키 네임스페이스가 다를 수 있습니다. CONFIG_USER_NS 설정을 확인하세요.

키링 서브시스템의 핵심 자료 구조(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을 승격시킨 결과입니다.

MOK → .machine 키링 승격 흐름 1. UEFI 변수 PK, KEK, db, dbx 펌웨어 수준 신뢰 루트 2. shim / MOK mokutil --import MokManager 확인 3. MokListTrusted end-user 승인 신뢰 승격 결정 4. 커널 .platform → .machine secondary 링크 키링별 역할 요약 .platform — 펌웨어가 보고한 UEFI db/KEK 키. 커널 업데이트에도 유지됩니다. .builtin_trusted_keys — 커널 이미지 빌드 시 내장된 키. 읽기 전용, 외부 주입 불가. .secondary_trusted_keys — 시스템 키가 보증한 CA 키를 더 얹는 런타임 링크. .machine — shim이 MokListTrusted로 승격시킨 MOK. 개발자 모듈 서명용 1차 영역.
# 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
PCR 정책 팁: PCR 7만 바인딩하면 Secure Boot 상태 변화를 감지할 수 있고, PCR 11을 섞으면 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을 선택 가능하게 추가하는 방향으로 정리되고 있습니다.

# 현재 커널 설정에서 모듈 서명 알고리즘 확인
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"
운영 팁: CA-enforced MOK와 crypto-policies를 같이 쓰면 "관리자가 조직 CA 키 하나만 승인하고, 그 CA가 서명한 하위 키들은 자동으로 받아들이는" 체계를 구성할 수 있습니다. 개별 개발자 키를 BIOS에서 승인받는 불편이 사라지지만, CA 개인키 관리가 단일 실패 지점이 되므로 HSM이나 TPM2 기반 키 격리가 필수입니다.

외부 참고 자료