Исходные данные
Дано:
-
конвейер CI/CD, реализованный, к примеру, в GitLab. Для
корректной работы ему требуются, как это очень часто бывает, некие
секреты - API-токены, пары логи/пароль, приватные SSH-ключи - да
всё, о чём только можно подумать;
-
работает этот сборочный конвейер, как это тоже часто бывает, на
базе контейнеров. Соответственно, чем меньше по размеру образы -
тем лучше, чем меньше в них всякой всячины - тем лучше.
Требуется консольная утилита, которая:
-
занимает минимум места;
-
умеет расшифровывать секреты, зашифрованные
ansible-vault
;
-
не требует никаких внешних зависимостей;
-
умеет читать ключ из файла.
Я думаю, что люди, причастные к созданию сборочных конвейеров,
по достоинству смогут оценить каждое из этих требований. Ну а что у
меня получилось в результате - читайте далее.
На всякий случай сразу напоминаю, что по действующему
законодательству разработка средств криптографической защиты
информации в РФ - лицензируемая деятельность.
Иначе говоря, без наличия лицензии вы не можете просто так взять и
продавать получившееся решение.
По поводу допустимости полных текстов расшифровщиков в статьях
вроде этой - надеюсь, что компетентные в этом вопросе читатели
смогут внести свои уточнения в комментариях.
Начнём сначала
Итак, предположим, что у нас на Linux-хосте с CentOS 7 уже
установлен Ansible, к примеру, версии 2.9 для Python версии 3.6.
Установлен, конечно же, с помощью virtualenv
в каталог
"/opt/ansible
". Дальше для целей удовлетворения
чистого научного любопытства возьмём какой-нибудь YaML-файл, и
зашифруем его с помощью утилиты ansible-vault
:
ansible-vault encrypt vaulted.yml --vault-password-file=.password
Этот вызов, как можно догадаться, зашифрует файл
vaulted.yml
с помощью пароля, который хранится в файле
.password
.
Итак, что получается после зашифровывания файла с помощью
утилиты ansible-vault
? На первый взгляд - белиберда
какая-то, поэтому спрячу её под спойлер:
Содержимое файла vaulted.yml
$ANSIBLE_VAULT;1.1;AES256613735363539633137393665366436613138616632663731303737306666343433373565363336643365393033623439356364663537353365386464623836640a356464633264626330383232353362636131353736383936656639623035303230613764323339313061613039666333383035656663376465393837636665300a633732313730626265636538363339383237306264633830653665343639303538636331373138663935666436613235366336663438376231303639666133633739623436303438623463323636336332643666663064393731363034623038653861373536643136393431636437346337323833333165386534353432386663343465333836643131643237313262386634396534383166303565306264303162383833643765613936373632626136663738363462626665366131646631663834316262663162353532366664386330323139643266636562653639306238653162316563613934323836303536613532623864303839313038336232616134626433353166383837643165643439363835643731316238316439633039
Ну а как именно эта белиберда работает "под капотом" - давайте
разбираться.
Открываем файл
/opt/ansible/lib/python3.6/site-packages/ansible/parsing/vault/__init__.py
,
и в коде метода encrypt
класса VaultLib
видим следующий вызов:
VaultLib.encrypt
... b_ciphertext = this_cipher.encrypt(b_plaintext, secret) ...
То есть результирующее содержимое нашего файла будет создано в
результате вызова метода encrypt
некоторого класса.
Какого именно - в общем-то, невелика загадка, ниже по файлу есть
всего один класс с именем VaultAES256
.
Смотрим в его метод encrypt
:
VaultAES256.encrypt
@classmethoddef encrypt(cls, b_plaintext, secret): if secret is None: raise AnsibleVaultError('The secret passed to encrypt() was None') b_salt = os.urandom(32) b_password = secret.bytes b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt) if HAS_CRYPTOGRAPHY: b_hmac, b_ciphertext = cls._encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv) elif HAS_PYCRYPTO: b_hmac, b_ciphertext = cls._encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv) else: raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in encrypt)') b_vaulttext = b'\n'.join([hexlify(b_salt), b_hmac, b_ciphertext]) # Unnecessary but getting rid of it is a backwards incompatible vault # format change b_vaulttext = hexlify(b_vaulttext) return b_vaulttext
То есть перво-наперво генерируется "соль" длиной 32 байта. Затем
из побайтного представления пароля и "соли" вызовом
_gen_key_initctr
генерируется пара ключей
(b_key1
, b_key2
) и вектор инициализации
(b_iv
).
Генерация ключей
Что же происходит в _gen_key_initctr
?
_gen_key_initctr:
@classmethoddef _gen_key_initctr(cls, b_password, b_salt): # 16 for AES 128, 32 for AES256 key_length = 32 if HAS_CRYPTOGRAPHY: # AES is a 128-bit block cipher, so IVs and counter nonces are 16 bytes iv_length = algorithms.AES.block_size // 8 b_derivedkey = cls._create_key_cryptography(b_password, b_salt, key_length, iv_length) b_iv = b_derivedkey[(key_length * 2):(key_length * 2) + iv_length] elif HAS_PYCRYPTO: # match the size used for counter.new to avoid extra work iv_length = 16 b_derivedkey = cls._create_key_pycrypto(b_password, b_salt, key_length, iv_length) b_iv = hexlify(b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]) else: raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in initctr)') b_key1 = b_derivedkey[:key_length] b_key2 = b_derivedkey[key_length:(key_length * 2)] return b_key1, b_key2, b_iv
Если по сути, то внутри этого метода вызов
_create_key_cryptography
на основе пароля, "соли",
длины ключа и длины вектора инициализации генерирует некий
производный ключ (строка 10 приведённого фрагмента). Далее этот
производный ключ разбивается на части, и получаются те самые
b_key1
, b_key2
и b_iv
.
Следуем по кроличьей норе дальше. Что внутри
_create_key_cryptography
?
_create_key_cryptography:
@staticmethoddef _create_key_cryptography(b_password, b_salt, key_length, iv_length): kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=2 * key_length + iv_length, salt=b_salt, iterations=10000, backend=CRYPTOGRAPHY_BACKEND) b_derivedkey = kdf.derive(b_password) return b_derivedkey
Ничего особенного. Если оставить в стороне всю мишуру, то в
итоге вызывается функция библиотеки OpenSSL под названием
PBKDF2HMAC
с нужными параметрами. Можете, кстати,
самолично в этом убедиться, открыв файл
/opt/ansible/lib/python3.6/site-packages/cryptography/hazmat/backends/openssl/backend.py.
Кстати, длина производного ключа, как видите, специально
выбирается таким образом, чтобы хватило и на b_key1
, и
на b_key2
, и на b_iv
.
Собственно шифрование
Движемся дальше. Здесь нас встречает вызов
_encrypt_cryptography
с параметрами в виде открытого
текста, обоих ключей и вектора инициализации:
_encrypt_cryptography
@staticmethoddef _encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv): cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND) encryptor = cipher.encryptor() padder = padding.PKCS7(algorithms.AES.block_size).padder() b_ciphertext = encryptor.update(padder.update(b_plaintext) + padder.finalize()) b_ciphertext += encryptor.finalize() # COMBINE SALT, DIGEST AND DATA hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND) hmac.update(b_ciphertext) b_hmac = hmac.finalize() return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext)
В принципе, тут нет ничего особенного: шифр инициализируется из
вектора b_iv
, затем первым ключом b_key1
шифруется исходный текст, а результат этого шифрования хэшируется с
помощью второго ключа b_key2
.
Полученные в итоге байты подписи и шифртекста преобразуются в
строки своих шестнадцатеричных представлений через
hexlify
. (см. строка 14 фрагмента выше)
Окончательное оформление файла
Возвращаемся к строкам 16-20 фрагмента VaultAES256.encrypt: три строки,
содержащие "соль", подпись и шифртекст, склеиваются вместе, после
чего снова преобразуются в шестнадцатеричное представление
(комментарий прямо подсказывает, что это - для обратной
совместимости).
Дальше дописывается заголовок (помните, тот самый -
$ANSIBLE_VAULT;1.1;AES256)
, ну и, в общем-то, всё.
Обратный процесс
После того, как мы разобрались в прямом процессе, реализовать
обратный будет не слишком сложно - по крайней мере, если выбрать
правильный инструмент.
Понятно, что Python нам не подходит, иначе можно было и огород
не городить: ansible-vault одинаково хорошо работает в обе стороны.
С другой стороны, никто не мешает на базе библиотек Ansible
написать что-либо своё - в качестве разминки перед "подходом к
снаряду" я так и сделал, и о результате напишу отдельную
статью.
Тем не менее, для написания предмета статьи я воспользовался
FreePascal. Ввиду того, что языковой холивар темой статьи не
является, буду краток: выбрал этот язык, во-первых, потому что
могу, а во-вторых - потому что получаемый бинарник удовлетворяет
заданным требованиям.
Итак, нам понадобятся: FreePascal версии 3.0.4 (эта версия в
виде готовых пакетов - самая свежая, нормально устанавливающаяся в
CentOS 7), и библиотека DCPCrypt версии 2.1 (на GitHub). Интересно, что
прямо вместе с компилятором (fpc
) и обширным набором
библиотек в rpm-пакете поставляется консольная среда разработки
fp
.
К сожалению, "искаропки" модули этой библиотеки не собираются
компилятором fpc
- в них нужны минимальные правки. С
другой стороны, я предполагаю, что без этих правок предмет статьи
перестаёт относиться к лицензируемым видам деятельности и начинает
представлять чисто академический интерес - именно поэтому
выкладываю статью без них.
Часть кода, относящуюся к генерированию производного ключа
(реализацию той самой функции PBKDF2), я нашёл в интернете, и поместил в
отдельный модуль под названием "kdf".
Вот этот модуль собственной персоной:
kdf.pas
{$MODE OBJFPC}// ALL CREDITS FOR THIS CODE TO https://keit.co/p/dcpcrypt-hmac-rfc2104/unit kdf;interfaceuses dcpcrypt2,math;function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring;function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;implementationfunction RPad(x: string; c: Char; s: Integer): string;var i: Integer;begin Result := x; if Length(x) < s then for i := 1 to s-Length(x) do Result := Result + c;end;function XorBlock(s, x: ansistring): ansistring; inline;var i: Integer;begin SetLength(Result, Length(s)); for i := 1 to Length(s) do Result[i] := Char(Byte(s[i]) xor Byte(x[i]));end;function CalcDigest(text: string; dig: TDCP_hashclass): string;var x: TDCP_hash;begin x := dig.Create(nil); try x.Init; x.UpdateStr(text); SetLength(Result, x.GetHashSize div 8); x.Final(Result[1]); finally x.Free; end;end;function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;const blocksize = 64;begin // Definition RFC 2104 if Length(key) > blocksize then key := CalcDigest(key, hash); key := RPad(key, #0, blocksize); Result := CalcDigest(XorBlock(key, RPad('', #$36, blocksize)) + message, hash); Result := CalcDigest(XorBlock(key, RPad('', #$5c, blocksize)) + result, hash);end;function PBKDF1(pass, salt: ansistring; count: Integer; hash: TDCP_hashclass): ansistring;var i: Integer;begin Result := pass+salt; for i := 0 to count-1 do Result := CalcDigest(Result, hash);end;function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring; function IntX(i: Integer): ansistring; inline; begin Result := Char(i shr 24) + Char(i shr 16) + Char(i shr 8) + Char(i); end;var D, I, J: Integer; T, F, U: ansistring;begin T := ''; D := Ceil(kLen / (hash.GetHashSize div 8)); for i := 1 to D do begin F := CalcHMAC(salt + IntX(i), pass, hash); U := F; for j := 2 to count do begin U := CalcHMAC(U, pass, hash); F := XorBlock(F, U); end; T := T + F; end; Result := Copy(T, 1, kLen);end;end.
Из бросающегося в глаза - обратите внимание, что в Pascal и его
потомках отсутствует классическое разделение на заголовочные файлы
и файлы собственно с кодом, в этом смысле модульная организация
роднит его с Python, и отличает от C.
Однако от питонячьего модуля паскалевский отличается ещё и тем,
что "снаружи" доступны только те функции/переменные, которые
объявлены в секции interface
. То есть по умолчанию
внутри модуля ты можешь хоть "на ушах стоять" - снаружи никто не
сможет вызвать твои внутренние API. Так устроен язык, а хорошо это
или плохо - вопрос вкуса, поэтому оценки оставим в стороне
(питонистам передают привет функции/методы, начинающиеся на "_" и
"__").
Заголовочная часть
Код, как обычно, под спойлером.
Заголовочная часть ("шапка", header)
program devault;uses math, sysutils, strutils, getopts, DCPcrypt2, DCPsha256, DCPrijndael, kdf;
Далее нам понадобится пара функций - hexlify
и
unhexlify
(набросаны, конечно, "на скорую руку"). Они
являются аналогами соответствующих функций Python - вторая
возвращает строку из шестнадцатеричных представлений байтов
входного аргумента, а первая - наоборот, переводит строку
шестнадцатеричных кодов обратно в байты.
hexlify/unhexlify
function unhexlify(s:AnsiString):AnsiString;var i:integer; tmpstr:AnsiString;begin tmpstr:=''; for i:=0 to (length(s) div 2)-1 do tmpstr:=tmpstr+char(Hex2Dec(Copy(s,i*2+1,2))); unhexlify:=tmpstr;end;function hexlify(s:AnsiString):AnsiString;var i:integer; tmpstr:AnsiString;begin tmpstr:=''; for i:=1 to (length(s)) do tmpstr:=tmpstr+IntToHex(ord(s[i]),2); hexlify:=tmpstr;end;
Назначение функций showbanner()
,
showlicense()
и showhelp()
очевидно из
названий, поэтому я просто приведу их без комментариев.
showbanner() / showlicense() / showhelp()
showbanner()
procedure showbanner();begin WriteLn(stderr, 'DeVault v1.0'); Writeln(stderr, '(C) 2021, Sergey Pechenko. All rights reserved'); Writeln(stderr, 'Run with "-l" option to see license');end;
showlicense()
procedure showlicense();begin WriteLn(stderr,'Redistribution and use in source and binary forms, with or without modification,'); WriteLn(stderr,'are permitted provided that the following conditions are met:'); WriteLn(stderr,'* Redistributions of source code must retain the above copyright notice, this'); WriteLn(stderr,' list of conditions and the following disclaimer;'); WriteLn(stderr,'* Redistributions in binary form must reproduce the above copyright notice, '); WriteLn(stderr,' this list of conditions and the following disclaimer in the documentation'); WriteLn(stderr,' and/or other materials provided with the distribution.'); WriteLn(stderr,'* Sergey Pechenko''s name may not be used to endorse or promote products'); WriteLn(stderr,' derived from this software without specific prior written permission.'); WriteLn(stderr,'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"'); WriteLn(stderr,'AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,'); WriteLn(stderr,'THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE'); WriteLn(stderr,'ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE'); WriteLn(stderr,'FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES'); WriteLn(stderr,'(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;'); WriteLn(stderr,'LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON'); WriteLn(stderr,'ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT'); WriteLn(stderr,'(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,'); WriteLn(stderr,'EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'); WriteLn(stderr,'Commercial license can be obtained from author');end;
showhelp()
procedure showhelp();begin WriteLn(stderr,'Usage:'); WriteLn(stderr,Format('%s <-p password | -w vault_password_file> [-f secret_file]',[ParamStr(0)])); WriteLn(stderr,#09'"password" is a text string which was used to encrypt your secured content'); WriteLn(stderr,#09'"vault_password_file" is a file with password'); WriteLn(stderr,#09'"secret_file" is a file with encrypted content'); WriteLn(stderr,'When "-f" argument is absent, stdin is read by default');end;
Дальше объявляем переменные и константы, которые будут
использоваться в коде. Привожу их здесь только для полноты текста,
потому что комментировать тут особо нечего.
Переменные и константы
var secretfile, passwordfile, pass, salt, b_derived_key, b_key1, b_key2, b_iv, hmac_new, cphrtxt, fullfile, header, tmpstr, hmac:Ansistring; Cipher: TDCP_rijndael; key, vector, data, crypt: RawByteString; fulllist: TStringArray; F: Text; c: char; opt_idx: LongInt; options: array of TOption;const KEYLENGTH=32; // for AES256const IV_LENGTH=128 div 8;const CONST_HEADER='$ANSIBLE_VAULT;1.1;AES256';
Код
Ну, почти код - всё ещё вспомогательная функция, которая в
рантайме готовит массив записей для разбора параметров командной
строки. Почему она здесь - потому что работает с переменными,
объявленными в секции vars
выше.
preparecliparams()
procedure preparecliparams();begin SetLength(options, 6); with options[1] do begin name:='password'; has_arg:=Required_Argument; flag:=nil; value:=#0; end; with options[2] do begin name:='file'; has_arg:=Required_Argument; flag:=nil; value:=#0; end; with options[3] do begin name:='passwordfile'; has_arg:=Required_Argument; flag:=nil; value:=#0; end; with options[4] do begin name:='version'; has_arg:=No_Argument; flag:=nil; value:=#0; end; with options[5] do begin name:='license'; has_arg:=No_Argument; flag:=nil; value:=#0; end; with options[6] do begin name:='help'; has_arg:=No_Argument; flag:=nil; value:=#0; end;end;
А вот теперь точно код самой утилиты:
Весь остальной код
begin repeat c:=getlongopts('p:f:w:lh?',@options[1],opt_idx); case c of 'h','?' : begin showhelp(); halt(0); end; 'p' : pass:=optarg; 'f' : secretfile:=optarg; 'w' : passwordfile:=optarg; 'v' : begin showbanner(); halt(0); end; 'l' : begin showlicense(); halt(0); end; ':' : writeln ('Error with opt : ',optopt); // not a mistake - defined in getops unit end; until c=endofoptions; if pass = '' then // option -p not set if passwordfile <> '' then try Assign(F,passwordfile); Reset(F); Readln(F,pass); Close(F); except on E: EInOutError do begin Close(F); writeln(stderr, 'Password not set and password file cannot be read, exiting'); halt(1); end; end else begin // options -p and -w are both not set writeln(stderr, 'Password not set, password file not set, exiting'); showhelp(); halt(1); end; try Assign(F,secretfile); Reset(F); except on E: EInOutError do begin writeln(stderr, Format('File %s not found, exiting',[secretfile])); halt(1); end; end; readln(F,header); if header<>CONST_HEADER then begin writeln(stderr, 'Header mismatch'); halt(1); end; fullfile:=''; while not EOF(F) do begin Readln(F,tmpstr); fullfile:=fullfile+tmpstr; end; Close(F); fulllist:=unhexlify(fullfile).Split([#10],3); salt:=fulllist[0]; hmac:=fulllist[1]; cphrtxt:=fulllist[2]; salt:=unhexlify(salt); cphrtxt:=unhexlify(cphrtxt); b_derived_key:=PBKDF2(pass, salt, 10000, 2*32+16, TDCP_sha256); b_key1:=Copy(b_derived_key,1,KEYLENGTH); b_key2:=Copy(b_derived_key,KEYLENGTH+1,KEYLENGTH); b_iv:=Copy(b_derived_key,KEYLENGTH*2+1,IV_LENGTH); hmac_new:=lowercase(hexlify(CalcHMAC(cphrtxt, b_key2, TDCP_sha256))); if hmac_new<>hmac then begin writeln(stderr, 'Digest mismatch - file has been tampered with, or an error has occured'); Halt(1); end; SetLength(data, Length(crypt)); Cipher := TDCP_rijndael.Create(nil); try Cipher.Init(b_key1[1], 256, @b_iv[1]); Cipher.DecryptCTR(cphrtxt[1], data[1], Length(data)); Cipher.Burn; finally Cipher.Free; end; Writeln(data);end.
Дальше будет странная таблица, но, кажется, это - самый удобный
способ рассказа об исходном коде.
Стр.
|
Назначение
|
|
2-13
|
разбор параметров командной строки с отображением нужных
сообщений;
|
|
14-34
|
проверка наличия пароля в параметрах, при отсутствии - попытка
прочесть пароль из файла, при невозможности - останавливаем
работу;
|
|
35-44
|
попытка прочесть зашифрованный файл, указанный в параметрах;
|
Небольшой чит: по умолчанию имя файла (переменная secretfile)
равно пустой строке; в этом случае вызов Assign(F,
secretfile) в строке 36 свяжет переменную F с
stdin
|
45-50
|
проверка наличия в файле того самого заголовка
$ANSIBLE_VAULT;1.1;AES256 ;
|
|
51-57
|
читаем всё содержимое зашифрованного файла и закрываем его;
|
|
58-63
|
разбираем файл на части: "соль", дайджест, шифртекст - всё
отдельно; при этом все три части нужно будет ещё раз прогнать через
unhexlify (помните примечание в VaultAES256.encrypt?)
|
|
64-73
|
вычисление производного ключевого материала; разбиение его на
части; расчёт дайджеста; проверка зашифрованного файла на
корректность дайждеста;
|
|
74-83
|
подготовка буфера для расшифрованного текста; расшифровка;
затирание ключей в памяти случайными данными; вывод расшифрованного
содержимого в поток stdout
|
|
Интересная информация для питонистов
Кстати, вы же слышали, что в Python 3.10 наконец-то завезли
оператор case (PEP-634)?
Интересно, что его ввёл сам BDFL, и произошло это примерно через 14
лет после того, как по результатам опроса на PyCon 2007
первоначальный PEP-3103 был отвергнут.
Собственно, теперь всё на месте, осталось собрать:
[root@ansible devault]# time fpc devault.pas
-Fudcpcrypt_2.1:dcpcrypt_2.1/Ciphers:dcpcrypt_2.1/Hashes
-MOBJFPC
Здесь имейте в виду, что форматирование Хабра играет злую шутку
- никакого разрыва строки после первого минуса нет.
Вывод компилятора
Free Pascal Compiler version 3.0.4 [2017/10/02] for x86_64Copyright (c) 1993-2017 by Florian Klaempfl and othersTarget OS: Linux for x86-64Compiling devault.pasCompiling ./dcpcrypt_2.1/DCPcrypt2.pasCompiling ./dcpcrypt_2.1/DCPbase64.pasCompiling ./dcpcrypt_2.1/Hashes/DCPsha256.pasCompiling ./dcpcrypt_2.1/DCPconst.pasCompiling ./dcpcrypt_2.1/Ciphers/DCPrijndael.pasCompiling ./dcpcrypt_2.1/DCPblockciphers.pasCompiling kdf.pasLinking devault/usr/bin/ld: warning: link.res contains output sections; did you forget -T?3784 lines compiled, 0.5 secreal 0m0.543suser 0m0.457ssys 0m0.084s
Вроде неплохо: 3,8 тысячи строк кода собраны до исполняемого
файла за 0.6 сек. На выходе - статически связанный бинарник,
которому для работы от системы требуется только ядро. Ну то есть
для запуска достаточно просто скопировать этот бинарник в файловую
систему - и всё. Кстати, я забыл указать его размер: 875К. Никаких
зависимостей, компиляций по несколько минут и т.д.
Ах да, чуть не забыл самое интересное! Запускаем, предварительно
сложив пароль в файл ".password":
[root@ansible devault]# ./devault -w .password -f vaulted.yml---collections:- name: community.general scm: git src: https://github.com/ansible-collections/community.general.git version: 1.0.0
Вот такой нехитрый YaML я использовал в самом начале статьи для
создания зашифрованного файла.
Исходный код для самостоятельного изучения можно взять здесь.
Хотите ещё Ansible? (осторожно, денежные вопросы!)
Теперь у вас есть возможность сделать пожертвование автору
статьи - оно дополнительно замотивирует меня чаще выбираться на
прогулку с чашкой кофе и булкой с корицей, чтобы отдохнуть перед
написанием следующей статьи и обдумать её содержание.
Если же хотите систематизировать и углубить свои знания Ansible
- я провожу тренинги по Ansible, пишите мне в Telegram.