|
39 | 39 | decode_key_info/1,
|
40 | 40 | garbage_collect_keks/1,
|
41 | 41 | garbage_collect_keys/2,
|
| 42 | + cleanup_retired_keys/0, |
42 | 43 | maybe_rotate_integrity_tokens/1,
|
43 | 44 | remove_old_integrity_tokens/1,
|
44 | 45 | get_key_ids_in_use/0]).
|
|
55 | 56 |
|
56 | 57 | -define(RUNNER, {cb_gosecrets_runner, ns_server:get_babysitter_node()}).
|
57 | 58 | -define(RESTART_WAIT_TIMEOUT, 120000).
|
| 59 | +-define(RETIRED_KEYS_RETENTION_MONTHS, |
| 60 | + ?get_param(retired_keys_retention_months, 3)). |
58 | 61 |
|
59 | 62 | start_link() ->
|
60 | 63 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
@@ -562,8 +565,144 @@ select_files_to_retire(Dir, _KeyIdsInUse, {error, Reason}) ->
|
562 | 565 | [Dir, Reason]),
|
563 | 566 | [].
|
564 | 567 |
|
| 568 | +cleanup_retired_keys() -> |
| 569 | + cleanup_retired_keys(calendar:universal_time(), |
| 570 | + ?RETIRED_KEYS_RETENTION_MONTHS). |
| 571 | + |
| 572 | +cleanup_retired_keys({{CurrentYear, CurrentMonth, _}, _}, RetentionMonths) -> |
| 573 | + ?log_debug("Cleanup retired keys"), |
| 574 | + RetiredDir = retired_keys_dir(), |
| 575 | + maybe |
| 576 | + {ok, Dirs} ?= file:list_dir(RetiredDir), |
| 577 | + CurrentMonths = CurrentYear * 12 + CurrentMonth, |
| 578 | + lists:foreach( |
| 579 | + fun (DirName) -> |
| 580 | + FullPath = filename:join(RetiredDir, DirName), |
| 581 | + maybe |
| 582 | + {dir, true} ?= {dir, filelib:is_dir(FullPath)}, |
| 583 | + {ok, {Year, Month}} ?= parse_retired_dir_name(DirName), |
| 584 | + DirMonths = Year * 12 + Month, |
| 585 | + {old, true} ?= |
| 586 | + {old, CurrentMonths - DirMonths > RetentionMonths}, |
| 587 | + AllFiles = file:list_dir(FullPath), |
| 588 | + ?log_info("Permanently removing retired keys directory ~s as " |
| 589 | + "it is older than ~b months. Keys to be " |
| 590 | + "removed: ~.0p", |
| 591 | + [FullPath, RetentionMonths, AllFiles]), |
| 592 | + ok ?= misc:rm_rf(FullPath) |
| 593 | + else |
| 594 | + {dir, false} -> |
| 595 | + ?log_warning("Invalid retired keys directory name " |
| 596 | + "(not a directory): ~s, will be " |
| 597 | + "ignored", [FullPath]); |
| 598 | + error -> |
| 599 | + ?log_warning("Invalid retired keys directory name: ~s, " |
| 600 | + "will be ignored", [DirName]); |
| 601 | + {old, false} -> |
| 602 | + ok; |
| 603 | + {error, Reason} -> |
| 604 | + ?log_error("Failed to remove retired keys directory ~s: " |
| 605 | + "~p", [FullPath, Reason]) |
| 606 | + end |
| 607 | + end, Dirs), |
| 608 | + ok |
| 609 | + else |
| 610 | + {error, enoent} -> |
| 611 | + ok; |
| 612 | + {error, Reason} -> |
| 613 | + ?log_error("Failed to list retired keys directory ~s: ~p", |
| 614 | + [RetiredDir, Reason]), |
| 615 | + ok |
| 616 | + end. |
| 617 | + |
| 618 | +parse_retired_dir_name(DirName) -> |
| 619 | + try |
| 620 | + [YearStr, MonthStr] = string:tokens(DirName, "-"), |
| 621 | + Year = list_to_integer(YearStr), |
| 622 | + Month = list_to_integer(MonthStr), |
| 623 | + case calendar:valid_date(Year, Month, 1) of |
| 624 | + true -> {ok, {Year, Month}}; |
| 625 | + false -> error |
| 626 | + end |
| 627 | + catch |
| 628 | + _:_ -> error |
| 629 | + end. |
| 630 | + |
565 | 631 | -ifdef(TEST).
|
566 | 632 |
|
| 633 | +cleanup_retired_keys_test() -> |
| 634 | + %% Create temp directory for test |
| 635 | + RetiredDir = retired_keys_dir(), |
| 636 | + ok = filelib:ensure_dir(RetiredDir ++ "/"), |
| 637 | + |
| 638 | + %% Helper to create test directories and files |
| 639 | + CreateTestDir = |
| 640 | + fun (YearMonth) -> |
| 641 | + Dir = filename:join(RetiredDir, YearMonth), |
| 642 | + ok = filelib:ensure_dir(Dir ++ "/"), |
| 643 | + ok = file:write_file(filename:join(Dir, "test.key.1"), <<"test">>) |
| 644 | + end, |
| 645 | + |
| 646 | + try |
| 647 | + %% Create test directories for different months |
| 648 | + lists:foreach(CreateTestDir, [ |
| 649 | + "2023-10", %% Should be removed (>3 months old) |
| 650 | + "2023-11", %% Should be removed (>3 months old) |
| 651 | + "2023-12", %% Should stay (3 months old) |
| 652 | + "2024-01", %% Should stay (2 months old) |
| 653 | + "2025-02" %% Should stay (in future) |
| 654 | + ]), |
| 655 | + |
| 656 | + %% Also create some invalid directory names that should be ignored |
| 657 | + lists:foreach(CreateTestDir, [ |
| 658 | + "not-a-date", |
| 659 | + "2023-13", |
| 660 | + "2023-0", |
| 661 | + "2023" |
| 662 | + ]), |
| 663 | + %% Create a file in retiredKeysDir, should be ignored |
| 664 | + ok = file:write_file(filename:join(RetiredDir, "test.key.1"), |
| 665 | + <<"test">>), |
| 666 | + |
| 667 | + %% Run cleanup with reference date of 2024-03-01 and 3 month retention |
| 668 | + ok = cleanup_retired_keys({{2024, 3, 1}, {0,0,0}}, 3), |
| 669 | + |
| 670 | + %% Verify correct directories were removed/kept |
| 671 | + ?assertNot(filelib:is_dir(filename:join(RetiredDir, "2023-10"))), |
| 672 | + ?assertNot(filelib:is_dir(filename:join(RetiredDir, "2023-11"))), |
| 673 | + ?assert(filelib:is_dir(filename:join(RetiredDir, "2023-12"))), |
| 674 | + ?assert(filelib:is_dir(filename:join(RetiredDir, "2024-01"))), |
| 675 | + ?assert(filelib:is_dir(filename:join(RetiredDir, "2025-02"))), |
| 676 | + |
| 677 | + %% Invalid directories and files should still exist since they were |
| 678 | + %% ignored |
| 679 | + ?assert(filelib:is_dir(filename:join(RetiredDir, "not-a-date"))), |
| 680 | + ?assert(filelib:is_dir(filename:join(RetiredDir, "2023-13"))), |
| 681 | + ?assert(filelib:is_dir(filename:join(RetiredDir, "2023-0"))), |
| 682 | + ?assert(filelib:is_dir(filename:join(RetiredDir, "2023"))), |
| 683 | + ?assert(filelib:is_file(filename:join(RetiredDir, "test.key.1"))) |
| 684 | + |
| 685 | + after |
| 686 | + %% Cleanup test directory |
| 687 | + ok = misc:rm_rf(RetiredDir) |
| 688 | + end. |
| 689 | + |
| 690 | +parse_retired_dir_name_test() -> |
| 691 | + ?assertEqual({ok, {2023, 12}}, parse_retired_dir_name("2023-12")), |
| 692 | + ?assertEqual({ok, {2024, 1}}, parse_retired_dir_name("2024-1")), |
| 693 | + ?assertEqual({ok, {2024, 1}}, parse_retired_dir_name("2024-01")), |
| 694 | + ?assertEqual(error, parse_retired_dir_name("2024-13")), |
| 695 | + ?assertEqual(error, parse_retired_dir_name("2024-0")), |
| 696 | + ?assertEqual(error, parse_retired_dir_name("2024")), |
| 697 | + ?assertEqual(error, parse_retired_dir_name("2024-")), |
| 698 | + ?assertEqual(error, parse_retired_dir_name("2024-")), |
| 699 | + ?assertEqual(error, parse_retired_dir_name("-12")), |
| 700 | + ?assertEqual(error, parse_retired_dir_name("abc-12")), |
| 701 | + ?assertEqual(error, parse_retired_dir_name("2024-abc")), |
| 702 | + ?assertEqual(error, parse_retired_dir_name("")), |
| 703 | + ?assertEqual(error, parse_retired_dir_name("2024-12-25")). |
| 704 | + |
| 705 | + |
567 | 706 | -define(A, "a0000000-0000-0000-0000-000000000000").
|
568 | 707 | -define(B, "b0000000-0000-0000-0000-000000000000").
|
569 | 708 | -define(C, "c0000000-0000-0000-0000-000000000000").
|
@@ -630,15 +769,15 @@ key_path(Kind) ->
|
630 | 769 | bucket_dek_id(Bucket, DekId) ->
|
631 | 770 | iolist_to_binary(filename:join([Bucket, "deks", DekId])).
|
632 | 771 |
|
| 772 | +retired_keys_dir() -> |
| 773 | + filename:join(path_config:component_path(data), "retired_keys"). |
| 774 | + |
633 | 775 | retire_key(Kind, Filename) ->
|
634 | 776 | Dir = key_path(Kind),
|
635 | 777 | FromPath = filename:join(Dir, Filename),
|
636 | 778 | {{Y, M, _}, _} = calendar:universal_time(),
|
637 | 779 | MonthDir = lists:flatten(io_lib:format("~b-~b", [Y, M])),
|
638 |
| - ToPath = filename:join([path_config:component_path(data), |
639 |
| - "retired_keys", |
640 |
| - MonthDir, |
641 |
| - Filename]), |
| 780 | + ToPath = filename:join([retired_keys_dir(), MonthDir, Filename]), |
642 | 781 | case filelib:ensure_dir(ToPath) of
|
643 | 782 | ok ->
|
644 | 783 | case misc:atomic_rename(FromPath, ToPath) of
|
|
0 commit comments