Module: KeySloth

Defined in:
lib/keysloth.rb,
lib/keysloth/cli.rb,
lib/keysloth/config.rb,
lib/keysloth/crypto.rb,
lib/keysloth/errors.rb,
lib/keysloth/logger.rb,
lib/keysloth/version.rb,
lib/keysloth/git_manager.rb,
lib/keysloth/file_manager.rb

Overview

Основной модуль KeySloth - Ruby gem для управления зашифрованными секретами

KeySloth предоставляет инструменты для безопасного хранения и управления секретами (сертификаты, ключи, конфигурационные файлы) в зашифрованном виде в Git репозиториях.

Examples:

Базовое использование

# Получение секретов из репозитория
KeySloth.pull(repo_url: '[email protected]:company/secrets.git',
              branch: 'main',
              password: 'secret_password',
              local_path: './secrets')

# Отправка секретов в репозиторий
KeySloth.push(repo_url: '[email protected]:company/secrets.git',
              branch: 'main',
              password: 'secret_password',
              local_path: './secrets')

Использование с конфигурационным файлом

# Создайте файл .keyslothrc:
# repo_url: "[email protected]:company/secrets.git"
# branch: "main"
# local_path: "./secrets"

# Теперь достаточно указать только пароль:
KeySloth.pull(password: 'secret_password', config_file: '.keyslothrc')

Обработка ошибок

begin
  KeySloth.pull(repo_url: repo_url, password: password, local_path: './secrets')
rescue KeySloth::CryptoError => e
  puts "Ошибка расшифровки: #{e.message}"
rescue KeySloth::RepositoryError => e
  puts "Ошибка репозитория: #{e.message}"
rescue KeySloth::FileSystemError => e
  puts "Ошибка файловой системы: #{e.message}"
end

Использование в CI/CD с переменными окружения

# Установите переменные окружения:
# export SSH_PRIVATE_KEY="$(cat ~/.ssh/id_rsa)"
# export SECRET_PASSWORD="your_password"
# export GIT_AUTHOR_NAME="CI Bot"
# export GIT_AUTHOR_EMAIL="[email protected]"

KeySloth.pull(
  repo_url: '[email protected]:company/secrets.git',
  password: ENV['SECRET_PASSWORD'],
  local_path: './secrets'
)

Author:

  • KeySloth Team

Since:

  • 0.1.0

Defined Under Namespace

Classes: AuthenticationError, CLI, Config, ConfigurationError, Crypto, CryptoError, FileManager, FileSystemError, GitManager, KeySlothError, Logger, NetworkError, RepositoryError, ValidationError

Constant Summary collapse

VERSION =

Версия gem'а KeySloth

Since:

  • 0.1.0

'0.2.0'

Class Method Summary collapse

Class Method Details

.pull(repo_url:, password:, local_path: nil, branch: nil, config_file: nil) ⇒ Boolean

Получает и расшифровывает секреты из удаленного Git репозитория

Parameters:

  • repo_url (String) —

    URL Git репозитория (SSH)

  • branch (String) (defaults to: nil) —

    Ветка для получения секретов (по умолчанию 'main')

  • password (String) —

    Пароль для расшифровки секретов

  • local_path (String) (defaults to: nil) —

    Локальный путь для сохранения расшифрованных секретов

  • config_file (String, nil) (defaults to: nil) —

    Путь к файлу конфигурации (опционально)

Returns:

  • (Boolean) —

    true при успешном выполнении

Raises:

Since:

  • 0.1.0



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/keysloth.rb', line 78

def pull(repo_url:, password:, local_path: nil, branch: nil, config_file: nil)
  start_time = Time.now
  logger = Logger.new
  config = Config.load(config_file)

  # Аудит логирование начала операции
  logger.audit('pull_start', {
                 repo_url: repo_url,
                 branch: branch,
                 local_path: local_path,
                 config_file: config_file
               })

  # Объединяем параметры с конфигурацией (параметры имеют приоритет, nil не перетирает)
  merged_config = config.merge({
    repo_url: repo_url,
    branch: branch,
    local_path: local_path
  }.compact)

  logger.info("Начинаем получение секретов из репозитория: #{repo_url}")

  git_manager = GitManager.new(merged_config[:repo_url], logger)
  # Прокидываем backup_count из конфигурации; невалидные значения заменяем дефолтом
  configured_backup_count = merged_config[:backup_count]
  backup_count = if configured_backup_count.is_a?(Integer) && configured_backup_count >= 0
                   configured_backup_count
                 else
                   KeySloth::FileManager::DEFAULT_BACKUP_COUNT
                 end
  file_manager = FileManager.new(logger, backup_count)
  crypto = Crypto.new(password, logger)

  # Создаем backup перед операцией
  if File.exist?(merged_config[:local_path])
    file_manager.create_backup(merged_config[:local_path])
  end

  # Клонируем/обновляем репозиторий и получаем зашифрованные файлы
  encrypted_files = git_manager.pull_encrypted_files(merged_config[:branch])

  # Создаем локальную директорию если не существует
  file_manager.ensure_directory(merged_config[:local_path])

  # Расшифровываем и сохраняем файлы с проверкой целостности
  integrity_failures = []

  encrypted_files.each do |encrypted_file|
    original_filename = encrypted_file[:name].gsub(/\.enc$/, '')

    # Проверяем целостность зашифрованного файла
    integrity_check = crypto.verify_integrity_detailed(encrypted_file[:content])

    unless integrity_check[:valid]
      error_msg = "Ошибка целостности для #{original_filename}: #{integrity_check[:error] || 'структура данных повреждена'}"
      logger.error(error_msg)
      integrity_failures << { file: original_filename, error: integrity_check[:error] }
      next
    end

    logger.debug("Проверка целостности пройдена для: #{original_filename}")

    # Расшифровываем файл
    decrypted_content = crypto.decrypt_file(encrypted_file[:content])
    local_file_path = File.join(merged_config[:local_path], original_filename)

    # Проверяем целостность расшифрованного файла
    if decrypted_content.nil? || decrypted_content.empty?
      logger.warn("Расшифрованный файл пустой: #{original_filename}")
    end

    file_manager.write_file(local_file_path, decrypted_content)
    logger.info("Расшифрован файл: #{original_filename} (размер: #{decrypted_content.length} байт)")
  end

  # Проверяем наличие ошибок целостности
  unless integrity_failures.empty?
    failure_details = integrity_failures.map { |f| "#{f[:file]}: #{f[:error]}" }.join('; ')
    raise CryptoError, "Обнаружены ошибки целостности файлов: #{failure_details}"
  end

  duration = Time.now - start_time
  logger.info("Успешно получено и расшифровано #{encrypted_files.size} файлов")

  # Аудит логирование успешного завершения
  logger.security_log('pull', :success, duration: duration, details: {
                        files_count: encrypted_files.size,
                        repo_url: repo_url,
                        branch: merged_config[:branch]
                      })

  true
rescue StandardError => e
  duration = Time.now - start_time
  logger.security_log('pull', :failure, duration: duration, details: {
                        error: e.class.name,
                        repo_url: repo_url,
                        branch: branch
                      })
  raise
ensure
  git_manager&.cleanup
end

.push(repo_url:, password:, local_path: nil, branch: nil, config_file: nil, commit_message: nil) ⇒ Boolean

Шифрует и отправляет секреты в удаленный Git репозиторий

Parameters:

  • repo_url (String) —

    URL Git репозитория (SSH)

  • branch (String) (defaults to: nil) —

    Ветка для отправки секретов (по умолчанию 'main')

  • password (String) —

    Пароль для шифрования секретов

  • local_path (String) (defaults to: nil) —

    Локальный путь с секретами для шифрования

  • config_file (String, nil) (defaults to: nil) —

    Путь к файлу конфигурации (опционально)

  • commit_message (String) (defaults to: nil) —

    Сообщение коммита (опционально)

Returns:

  • (Boolean) —

    true при успешном выполнении

Raises:

Since:

  • 0.1.0



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/keysloth.rb', line 194

def push(repo_url:, password:, local_path: nil, branch: nil,
         config_file: nil, commit_message: nil)
  start_time = Time.now
  logger = Logger.new
  config = Config.load(config_file)

  # Аудит логирование начала операции
  logger.audit('push_start', {
                 repo_url: repo_url,
                 branch: branch,
                 local_path: local_path,
                 config_file: config_file,
                 commit_message: commit_message
               })

  # Объединяем параметры с конфигурацией (параметры имеют приоритет, nil не перетирает)
  merged_config = config.merge({
    repo_url: repo_url,
    branch: branch,
    local_path: local_path
  }.compact)

  logger.info("Начинаем отправку секретов в репозиторий: #{repo_url}")

  git_manager = GitManager.new(merged_config[:repo_url], logger)
  configured_backup_count = merged_config[:backup_count]
  backup_count = if configured_backup_count.is_a?(Integer) && configured_backup_count >= 0
                   configured_backup_count
                 else
                   KeySloth::FileManager::DEFAULT_BACKUP_COUNT
                 end
  file_manager = FileManager.new(logger, backup_count)
  crypto = Crypto.new(password, logger)

  # Проверяем существование локальной директории
  unless file_manager.directory_exists?(merged_config[:local_path])
    raise FileSystemError, "Локальная директория не существует: #{merged_config[:local_path]}"
  end

  # Получаем список файлов для шифрования
  local_files = file_manager.collect_secret_files(merged_config[:local_path])

  if local_files.empty?
    logger.warn('Не найдено файлов секретов для отправки')
    return true
  end

  # Клонируем репозиторий и переключаемся на нужную ветку
  git_manager.prepare_repository(merged_config[:branch])

  # Шифруем и подготавливаем файлы
  encrypted_files = local_files.map do |file_path|
    content = file_manager.read_file(file_path)
    encrypted_content = crypto.encrypt_file(content)
    relative_path = file_manager.get_relative_path(file_path, merged_config[:local_path])
    encrypted_filename = "#{relative_path}.enc"

    {
      path: encrypted_filename,
      content: encrypted_content
    }
  end

  # Записываем зашифрованные файлы в репозиторий
  git_manager.write_encrypted_files(encrypted_files)

  # Создаем коммит и отправляем
  commit_msg = commit_message || "Update secrets: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
  git_manager.commit_and_push(commit_msg, merged_config[:branch])

  duration = Time.now - start_time
  logger.info("Успешно зашифровано и отправлено #{encrypted_files.size} файлов")

  # Аудит логирование успешного завершения
  logger.security_log('push', :success, duration: duration, details: {
                        files_count: encrypted_files.size,
                        repo_url: repo_url,
                        branch: merged_config[:branch],
                        commit_message: commit_msg
                      })

  true
rescue StandardError => e
  duration = Time.now - start_time
  logger.security_log('push', :failure, duration: duration, details: {
                        error: e.class.name,
                        repo_url: repo_url,
                        branch: branch
                      })
  raise
ensure
  git_manager&.cleanup
end