Shell Scripting w przykładach: jak w szybki sposób wygenerować certyfikaty SSL dla serwerów
Administrator to człowiek szanujący swój czas i z reguły pragmatyczny. Mam tutaj na myśli definicję pragmatyzmu odnoszącą się do tego, że podejmowane działania mają przynosić korzyść. Właśnie po to wymyślono skryptowanie – już nie jest ważne w czym, chociaż my w tym cyklu jak sama nazwa wskazuje rozmawiamy o skryptach powłoki.
Efekt ma być taki, że jeżeli coś jest powtarzalne i do tego żeby to zrobić trzeba się naklepać (co zajmuje cenny czas, a jeszcze przy korzystaniu z klawiatury potencjalnie można gdzieś się pomylić), należy to oskryptować.
Jak odgrażałem się w artykule: „#AdminCases czyli gotowa solucja: jak stworzyć certyfikaty SSL dla serwera bez udziału komercyjnego CA, aby przeglądarki uważały, że jestem zaufany”, to co zostało tam pokazane jako żmudne wprowadzanie poleceń poprzez tłuczenie w klawisze, można zawrzeć w skrypcie Shell’owym. Nie dość, że napisanie takowego będzie banalnie proste, to jeszcze wprowadzając dane, niezbędne do tego co jest tematem naszego przykładu, nie popełnimy błędu, a o ten zawsze jest łatwo.
Zatem do dzieła.
W tym przypadku będę tworzył skrypt na jednym z Linuxów z rodziny Ubuntu, ale to naprawdę nie ma znaczenia, grunt, że skorzystam z powłoki Bash zatem shebang line będzie wyglądała w ten sposób:
#!/bin/bashTeraz musimy się zastanowić nad tym, jakie dane będą nam niezbędne na wejściu, no i druga strona medalu, czyli co może zostać zapisane jako atrybuty stałe. We wspomnianym artykule z cyklu #AdminCases wpisywaliśmy wszystko „z palca”, a tutaj chcemy to zautomatyzować wszystko co można uznać za stałe dane, które powinny rzeczywiście takimi się stać.
Co potrzebujemy wiedzieć, co jest niezbędne do uruchomienia, no i co będziemy robić w dalszej części (oczywiście w telegraficznym skrócie):
- zakładamy, że mamy wygenerowany nasz CA, ponieważ do podpisania będziemy potrzebowali klucza prywatnego oraz wygenerowanego certyfikatu,
- potrzebujemy klucza serwera oraz żądania podpisu certyfikatu,
- w przypadku generowania żądania wymagane było wypełnienie kilku pól zatem niezbędne jest wiedzieć jakimi danymi owe pola wypełnić,
- do żądania również niezbędny jest plik z dodatkowymi ustawieniami gdzie potrzebujemy nazwy i ip serwera.
Dane, które muszą znaleźć się na wejściu skryptu to w naszym przypadku nazwa oraz adres ip serwera, a to dlatego, że serwerów w projekcie możemy mieć multum, a ten skrypt ma dla każdego dla którego sobie życzymy wygenerować certyfikat SSL i go podpisać własnym CA. Stałymi danymi – zatem tymi, które umieścimy w treści programu - będą pola o których wypełnienia żąda polecenie openssl po jego uruchomieniu.
Zastanówmy się jak byłoby najlogiczniej wprowadzić zmienne zawierające nazwę serwera oraz jego ip adres. Chciałbym, aby skrypt był uniwersalny, co w tym przypadku rozumiem jako opcję podania na wejściu 2 zmiennym w postaci: komenda zmienna_1 zmienna _2, a jeżeli tego nie uczynimy to aby skrypt wymusił na nas wprowadzenie tychże danych. Do tego będziemy potrzebowali warunku, który sprawdzi czy na wejściu mamy 2 atrybuty. Skorzystamy więc ze zmiennej $#, która to wartość parametrów wejściowych przechowuje oraz z dobrodziejstwa polecenia test, który służy właśnie do porównywania w Shell’u. Stara funkcjonalność np. wykorzystywana w Born lub Korn Shell to ta, która charakteryzuje się notacją z pojedynczym nawiasem kwadratowym, a owa funkcjonalność miała swoje ułomności, o których wspomnę w dalszej części wskazując kilka różnic w stosunku do nowej. My skorzystamy zapisu nowego z podwójnymi nawiasami kwadratowymi. Zatem nasz kod sprawdzający będzie wyglądał w ten sposób:
if [[ $# -lt 2 ]]
then
echo "Server name: "
read NAME
echo "Server ip: "
read IP
else
NAME=$1
IP=$2
fi
No i co się tutaj dzieje: za pomocą polecenia if sprawdzamy ilość atrybutów i jeżeli ta jest mniejsza od 2 (-lt) to wymuszamy wprowadzenie argumentów nazwy oraz ip serwera w innym przypadku (czyli użytkownik podał 2 atrybuty) przypisujemy wprowadzone dane do zmiennych.
Dalej będziemy już tworzyć wszystko to co było opisane przytaczanym przeze mnie artykule innymi słowy będziemy wykorzystywać polecenie openssl:
- tworzymy klucz serwera – tutaj skorzystamy z danych przypisanych do zmiennej $NAME
openssl genrsa -out "$NAME.key" 2048- generujemy żądanie podpisania certyfikatu – i tutaj musimy się na chwilę zatrzymać, ponieważ w artykule wprowadzaliśmy wszystko z ręki, a w przypadku skryptu byłoby to bez sensu, bo wtedy po co nam skrypt skoro niczego by nie usprawniał tylko wywoływał po kolei polecenia, a my nadal wszystkie niezbędne dane musielibyśmy wprowadzać. Dlatego do wymienionego w artykule polecenia dodamy atrybut -subj gdzie wszystkie dane zostaną uzupełnione za pomocą jednego stringu, co zapobiegnie wprowadzaniu ręcznemu.
openssl req -new -key "$NAME.key" -out "$NAME.csr" -subj "/C=PL/ST=Narnia/L=Pingwinowo/O=Nielot Company/OU=Garbage Department/CN=$NAME"- tworzymy plik z dodatkowymi ustawieniami – zamiast tworzyć go ręcznie utworzymy zawartość dynamicznie wprowadzając niezbędne dane. Skorzystamy z funkcjonalności przekierowań, aby utworzyć plik o nazwie wskazanej przez zmienną $NAME z rozszerzeniem .ext i zawartością w postaci wielowierszowego stringu. W wersji z artykułu z cyklu #AdminCases w tym pliku z dodatkowymi ustawieniami serwera mieliśmy definicję zmiennej subjectAltName ze wskazaniem na @alt_names oraz definicje alt_names w postaci nazwy serwera oraz ip. Tutaj jeden z moich szacownych kolegów zwrócił uwagę, że dobrze byłoby ustawić dodatkowe atrybuty związane z certyfikacją zatem umieszczam takowe w postaci definicji authorityKeyIdentifier, basicConstraints oraz keyUsage. Można również użyć atrybutu extendedKeyUsage rozszerzające informacje o przeznaczeniu klucza np. extendedKeyUsage = critical, serverAuth
cat >"$NAME.ext" <<-EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $NAME
IP.1 = $IP
EOF
- podpisujemy certyfikat używając własnego CA – jednak, aby to zrobić musimy mieć pewność, że klucz prywatny oraz certyfikat CA egzystują, w katalogu w którym się znajdujemy więc znowu musimy użyć instrukcji warunkowej. I tutaj możemy zobaczyć różnicę notacji, jaka istnieje w starej i nowej formie, czyli używając [ ] lub [[ ]].
W starej formie taki if wyglądałby następująco:
if [ -f test.pem -a -f testkey.pem ]W nowej formule będzie wyglądał:
if [[ -f test.pem && -f testkey.pem ]]Dodatkowo w nowej funkcjonalności nie ma problemów z pustymi stringami lub stringami zawierającymi spacje, na które stara opcja była uczulona i trzeba było taką zmienną umieszczać w cudzysłowie. Nowa funkcjonalność przyniosła również możliwość dopasowania zmiennej wzoru np.
if [[ $ANSWER = y* ]]Jest jeszcze kilka innych ciekawych możliwości, więc zapraszam do pogłębiania wiedzy i poszukiwań tych przez Was nieodkrytych.
Wracając do tematu, to jeżeli istnieją sprawdzane pliki uruchamiamy polecenie openssl podpisujące certyfikat, jeżeli nie to wyrzucamy błąd z informacją o braku certyfikatów.
if [[ -f test.pem && -f testkey.pem ]]
then
openssl x509 -req -in $NAME.csr -CA test.pem -CAkey testkey.pem -CAcreateserial -out $NAME.crt -days 825 -sha256 -extfile $NAME.ext
else
echo "Brak certyfikatow CA"
exit 99
fi
No i szczęśliwie dotarliśmy do końca naszego skryptu. Kupa radości z przewagą radości. Pewnie chcecie zobaczyć go w całej okazałości (dla tych leniwych, którzy lubią kopiować, zamiast napisać) zatem proszę bardzo:
#!/bin/bash
if [[ $# -lt 2 ]]
then
echo "Server name: "
read NAME
echo "Server ip: "
read IP
else
NAME=$1
IP=$2
fi
openssl genrsa -out "$NAME.key" 2048
openssl req -new -key "$NAME.key" -out "$NAME.csr" -subj "/C=PL/ST=Narnia/L=Pingwinowo/O=Nielot Company/OU=Garbage Department/CN=$NAME"
cat >"$NAME.ext" <<-EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $NAME
IP.1 = $IP
EOF
if [[ -f test.pem && -f testkey.pem ]]
then
openssl x509 -req -in $NAME.csr -CA test.pem -CAkey testkey.pem -CAcreateserial -out $NAME.crt -days 825 -sha256 -extfile $NAME.ext
else
echo "Brak certyfikatow CA"
exit 99
fi
Pozostaje nam uruchomić nasz skrypcik (oczywiście, jeżeli nadaliście mu uprawnienia execute).
Jak już wspomniałem wcześniej są na to 2 sposoby:
- polecenie z atrybutem nazwa i ip – zacznie od razu generować certyfikaty
- polecenie bez atrybutów – pojawi się monit o padanie nazwy serwera, a następnie ip
Pierwszy sposób możemy wykorzystać chociażby do hurtowego wygenerowania certyfikatów na podstawie listy. Żeby nie mącić i nie przedłużać postaram się to zwizualizować w innym artykule może dodatkowo pokazując w jaki sposób zrobić obsługę błędów, która to zawsze jest jedną z najważniejszych zagadnień programowania.
Przystąpmy do uruchomienia według sposobu numer 2. Umieszczam tutaj to co pojawiło się na ekranie, czyli prośba o wprowadzenie danych, a gdy te zostaną one uzupełnione to powstaje plik z certyfikatem serwera.
aciek@nielot-lenovo:~/test_cert$ ./create_cert_for_server
Server name:
server1
Server ip:
192.168.0.100
Generating RSA private key, 2048 bit long modulus (2 primes)
..........................................................................................................+++++
.............................................+++++
e is 65537 (0x010001)
Signature ok
subject=C = PL, ST = Narnia, L = Pingwinowo, O = Nielot Company, OU = Garbage Department, CN = server1
Getting CA Private Key
Po zakończeniu listing zawartości katalogu bieżącego powinien wyglądać mniej więcej tak:
aciek@nielot-lenovo:~/test_cert$ ls -l
total 32
-rwxr-xr-x 1 aciek aciek 817 Feb 28 17:45 create_cert_for_server
-rw-rw-r-- 1 aciek aciek 1700 Feb 28 18:29 server1.crt
-rw-rw-r-- 1 aciek aciek 1029 Feb 28 18:29 server1.csr
-rw-rw-r-- 1 aciek aciek 218 Feb 28 18:29 server1.ext
-rw------- 1 aciek aciek 1679 Feb 28 18:29 server1.key
-rw------- 1 aciek aciek 3272 Feb 28 00:33 testkey.pem
-rw-rw-r-- 1 aciek aciek 2000 Feb 28 00:33 test.pem
-rw-rw-r-- 1 aciek aciek 41 Feb 28 18:29 test.srl
Wystarczy teraz zaimplementować taki certyfikat na serwerze i możemy w najlepsze cieszyć się połączeniami SSL – oczywiście, aby ten nie monitował w przeglądarce braku zaufania muszą zostać spełnione warunki, które zostały opisane w „#AdminCases czyli gotowa solucja: jak stworzyć certyfikaty SSL dla serwera bez udziału komercyjnego CA, aby przeglądarki uważały, że jestem zaufany”.
Od teraz w kilka sekund możemy stworzyć sobie certyfikat dla dowolnego serwera, dla którego potrzebujemy takowego, wprowadzając tylko 2 atrybuty, więc prawdopodobieństwo błędu jest znacznie ograniczone. Proste prawda no i bez udziwnień co wpisuje się we wspomnianą w pierwszym artykule z cyklu Shell Scripting w przykładach regułę KISS, której polskim odpowiednikiem jest BUZI , a rozwinięcie tego akronimu znajdźcie sobie sami ;)
Do zobaczenia i niech Shell będzie z Wami ;)
