bezpieczne hardocodowanie hasła?
uruchamiając skrypt, najbezpieczniej jest NIE hardcodować hasła. jeśli coś jest w skrypcie, to znaczy, że może być wykorzystane w nieautoryzowany sposób. jednak przykłady z życia pokazują, że czasem taka jest konieczność .. czy po prostu zlecenie (; przykład – obsługa wsparcia, która ma korzystać ze skryptu, który gdzieś tam sobie sięga. to 'gdzieś tam’ to środowisko, w którym nawet owa obsługa nie ma konta.
podstawowe zasady bezpieczeństwa to 3xA (AAA) – Authentication, Authorization & Accounting. jeśli utworzymy konto serwisowe, które ma coś wykonywać w imieniu uruchamiającego, to aby nie złamać zasady AAA i być w stanie stwierdzić kto wykonał daną akcję, trzeba to przesunąć do miejsca, gdzie uruchamia się skrypt/aplikację i umieć to powiązać… dalej mogłoby być o SIEM i tak dalej… ale nie o tym tutaj. ogólnie najważniejsze – wykorzystanie konta serwisowego (w środowisq legacy-domain), czy aplikacji proxy (tak jak działa to w Azure) nie musi koniecznie być złem, ale pozostaje pytanie… *jak zamieścić hasło skrypcie tak, aby nie wyciekło wraz ze skryptem?*
najbezpieczniej byłoby wykorzystać certyfikat… tak to można zrobić np. dla aplikacji Azure. niestety w środowisq klasycznym, gdzie mamy np. wykonać akcję w domenie, nie jest to łatwe. najbezpieczniej byłoby umieszczenie serwisu aka proxy, który działa sobie na serwerze w owej domenie i do tegoż serwisu uwierzytelniać się certyfikatem. spoko ale to developerka i do 'małych projekcików’ czy 'drobnych narzędzi’ – po prostu jest za duża czasochłonność (czyt. koszt), nie wspominając o tym że napisanie własnego serwisu (np. wystawiającego REST API) to nie jest już skryptowanie. to developerka.
no więc zróbmy to, co da się zrobić tak, aby było szybko i *możliwie* bezpiecznie.
…na ile się da
skoro nie jesteśmy w stanie uwierzytelnić się certyfikatem, to przynajmniej zaszyfrujmy hasło certyfikatem. co nam to da:
- dany skrypt będzie działał wyłącznie na tej maszynie gdzie jest certyfikat i dla tych osób, którym na to pozwolimy.
żeby była jasność – jeśli ktoś, kto ma złe intencje będzie tego skryptu używał, to sobie hasło odkoduje. ale po pierwsze – 'pierwszy lepszy’ tego nie zrobi, po drugie – jeśli założylibyśmy takiej osobie konto, to i tak wykorzysta je do niecnych celów. trzeba wiedzieć że to może być rozwiązanie *good enough* i tylko w tym kontekście można nazwać je 'bezpiecznym’. spory disclaimer ale nie chcę przypadkowych czytelników wprowadzić w błąd.
jak już wyjaśniłem co rozumiem przez 'bezpieczne’, recepta:
- załóż konto w domenie, bez żadnych dodatkowych uprawnień
- dobrze byłoby poustawiać 'deny’ dla takiego konta, przykrajając odpowiednio uprawnienia – wedle potrzeby, uznania i paranoia-level
- nadać uprawnienia do tego, do czego będzie wykorzystywane
- wygenerować odpowiedni certyfikat, zainstalować go i odpowiednio nadać uprawnienia
- zaszyfrować hasło certyfikatem
- i voila.. można go wykorzystać w skrypcie
punkty 1-3 pominę, natomiast 4-6 zaraz opiszę.
dzięki takiej konfiguracji – skrypt, nawet skopiowany czy przypadkowo udostępniony, na niewiele się zda. aby wydobyć hasło – trzeba mieć dostęp do klucza prywatnego certyfikatu.
generowanie certyfikatu
super proste, przy czym do tego celu cert musi spełniać odpowiednie funkcje, czyli (nomen-omen) kluczem jest dobry key-usage i document type:
$cert=New-SelfSignedCertificate -DnsName "script automation" -CertStoreLocation "Cert:\CurrentUser\My" -KeyUsage KeyEncipherment,DataEncipherment,KeyAgreement -Type DocumentEncryptionCert
$cert|Export-PfxCertificate -FilePath C:\temp\scriptautomation.pfx -Password (ConvertTo-SecureString -String "secret-string" -Force -AsPlainText)
dodatkowo można certyfikat usunąć z własnego komputera tak, aby jedna kopia była w pliq:
$cert|remove-Item
…albo zachować go jako backup a usunąć potem plik. up2u.
instalacja w środowisq docelowym
najłatwiej oczywiście kliknąć sobie na pliq pfx i wybrać opcję 'local machine’. jeśli umieścimy w 'current user’ to będzie dostępny tylko dla nas.

kolejną hiper-istotną kwestią jest *NIE włączanie* możliwości exportu klucza.
jeśli ktoś woli z PowerShell – należy użyć ’import-pfxCertificate’ *bez* parametru -exportable.
Import-PfxCertificate -FilePath C:\temp\scriptautomation.pfx -Password (ConvertTo-SecureString -string "secret-string" -force -AsPlainText) -CertStoreLocation Cert:\LocalMachine\My\
ciekawostka: w PS 5.1 ten commandlet ma dodatkowy parametr, nieopisany w doc, protectPrivateKey, który pozwala wykorzystać Virtual Secure Mode.
to połowa sukcesu – bo do tak zaimportowanego certyfikatu, nikt bez uprawnień administratora nie będzie mógł skorzystać. należy nadać uprawnienia do klucza prywatnego:
- odpal 'certlm.msc’ (zarządzanie certyfikatami komputera)
- wybierz certyfikat i kliknij PGM
- jest tam opcja 'manage private keys…’

4. nadaj uprawnienie READ (nie FC!) dla wybranych osób

kodowanie i odkodowanie hasła
teraz już z górki. przygotowanie środowiska to bardziej wymagający element, ponieważ hasło można zaszyfrować za pomocą polecenia ’protect-cmsMessage’:
C:\_scriptz :))o- Protect-CmsMessage -To 'cn=script automation' -Content 'moje tajne hasło'
-----BEGIN CMS-----
MIIBtAYJKoZIhvcNAQcDoIIBpTCCAaECAQAxggFMMIIBSAIBADAwMBwxGjAYBgNVBAMMEXNjcmlw
dCBhdXRvbWF0aW9uAhB2BdZafxn2nkcWjQNvBi+pMA0GCSqGSIb3DQEBBzAABIIBAFY0TU8LAPl4
4IAWp9tGDOZK/bAEwQpZPix5dz8AoOpaDckpZ8dqatggEKq7vo4VX6wL4sBkImQoZrCZjkT7oCSi
N1YFDOIghjD7e2mIvEl/Gq2bReUbsFQ0iw6Wg1K8zVLBzDQAIUHgJvaV2Ssf4nCJ8WfW5d9UzmBB
JpHAmy7cdofboQt6hRz0wafGNivD47z2xNSSVyhqHVUzBt6NWmAYouFMGYGsBZBuOHXnQJzh+saG
Q5VpWxZGRHzM3igl8IgYDF75R+b/FSfciPu5Se2eeD6xxHrPj9rfLm37KVIab/4+QA/lRFVt4cwA
ztjhh8puyPqYgKCE7t0Tp5VNQv4wTAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQjmYztOm64boC
6XqnH/9kMYAgYTQlOXeGex7qMH5S5gVRKy/JOhfIdqlabP9e/N/6iRk=
-----END CMS-----
ciekawostka: przy tym mechaniźmie szyfrowania (Cryptographic Message Syntax), uruchomienie tego polecenia, da za każdym razem inny wynik
jeśli ktoś nie pamięta nazwy certu, to można je szybko wylistować:
C:\_scriptz :))o- ls Cert:\LocalMachine\My\
PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\My
Thumbprint Subject
---------- -------
B643393E34BDC10BE2266F307F63A712578416F1 CN=script automation
wkleić sobie ten ciąg do skryptu i kiedy potrzebne jest hasło, użyć unprotect-CMSMessage… np. tak:
[string]$encPass="-----BEGIN CMS-----
MIIBtAYJKoZIhvcNAQcDoIIBpTCCAaECAQAxggFMMIIBSAIBADAwMBwxGjAYBgNVBAMMEXNjcmlw
dCBhdXRvbWF0aW9uAhB2BdZafxn2nkcWjQNvBi+pMA0GCSqGSIb3DQEBBzAABIIBAFY0TU8LAPl4
4IAWp9tGDOZK/bAEwQpZPix5dz8AoOpaDckpZ8dqatggEKq7vo4VX6wL4sBkImQoZrCZjkT7oCSi
N1YFDOIghjD7e2mIvEl/Gq2bReUbsFQ0iw6Wg1K8zVLBzDQAIUHgJvaV2Ssf4nCJ8WfW5d9UzmBB
JpHAmy7cdofboQt6hRz0wafGNivD47z2xNSSVyhqHVUzBt6NWmAYouFMGYGsBZBuOHXnQJzh+saG
Q5VpWxZGRHzM3igl8IgYDF75R+b/FSfciPu5Se2eeD6xxHrPj9rfLm37KVIab/4+QA/lRFVt4cwA
ztjhh8puyPqYgKCE7t0Tp5VNQv4wTAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQjmYztOm64boC
6XqnH/9kMYAgYTQlOXeGex7qMH5S5gVRKy/JOhfIdqlabP9e/N/6iRk=
-----END CMS-----"
try {
$cert = Get-ChildItem Cert:\LocalMachine\My\B643393E34BDC10BE2266F307F63A712578416F1 -ErrorAction stop
} catch {
write-log "missing authentication certificate. quitting" -type error
retrun $null
}
if(-not $cert.HasPrivateKey) {
write-log "missing decription factor. quitting." -type error
return $null
}
$certCN = $cert.Subject
try {
[securestring]$secStringPassword = ConvertTo-SecureString (Unprotect-CmsMessage -Content $encPass -To $certCN -ErrorAction stop) -AsPlainText -Force
} catch {
write-log "error decrypting auth password $($_.exception)" -type error
return $null
}
[pscredential]$creds = New-Object System.Management.Automation.PSCredential ('CORP\SVC-contactCreator', $secStringPassword)
inne scenariusze i metody
jakiś czas temu linkowałem dostęp do EXO certyfikatem. wygodne.
a podczas uruchamiania własnych skryptów, i tam gdzie wystarczą zwykłe credsy, nie chce mi się wpisywać hasła. zapisuję więc je sobie do pliq:
if(test-path $credsFile) {
$myCreds = Import-CliXml -Path $credsFile
write-log "used saved credentials in $credsFile" -type info
} else {
$myCreds=Get-Credential
if($NULL -eq $myCreds) {
write-log 'Cancelled.' -type error
exit -3
}
if($saveCredentials) {
$myCreds | Export-Clixml -Path $credsFile
write-log "credentials saved as $credsFile" -type info
}
}
get-Credential używa DPAPI i w 'zamkniętym’ środowisq (np. na własnym lapq) w zupełności wystarcza. z tego co kojarzę szyfrowane jest kluczem użytkownika i komputera więc taki plik, aby był odczytany, musi być w tym samym środowisq. mimo wszystko w 'otwartym’ środowisq raczej nie należy stosować takich metod.
eN.
PS Borderlands: $null.. czy nie?
jak sprawdzić czy zmienna została prawidłowo zainicjowana czy może jest wyzerowana? sposobów jest oczywiście kilka, zależnie od kontekstu. ale ponieważ trafiłem na brzegową sytuację, kilka ciekawostek…
pierwsze pytanie – co to znaczy 'wyzerowana zmienna’? od razu przychodzi mi do głowy pytanie-killer, dobre na sprawdzenie sprytu początqjącego deva – 'ile wartości można zakodować na jedno-bitowej zmiennej?’. dość oczywistą odpowiedzią jest 'dwie – 0 i 1′, ale prawidłową jest 'trzy – zero, jeden i null’. [oczywiście przykład wyssany z palca i można z tym polemizować, ale chodzi o ideę]. pokazuje to pewien niuans w sprawdzaniu wartości zmiennej, który może nieźle namieszać – zmienna pusta [empty] to nie to samo co zainicjowana zmienna z wartością $null [o ile $null można nazwać wartością, ale nie chodzi o filozofowanie tylko praktyczne konsekwencje].
początkowo zatem używałem prostego porównania:
potem dowiedziałem się, że $null powinien być zawsze po lewej stronie, ponieważ w specyficznych przypadkach zachowa się 'nieoczekiwanie’ – polecam poczytać na docsach. dla tego poprawnym zapisem jest:
przeważnie jednak, ze względu na logikę działania skryptów, chcę sprawdzić czy coś nie jest $null LUB puste. zacząłem więc przez długi czas używać magii PowerShell:
co w zasadzie powinno uniwersalnie sprawdzić zmienną. tego zapisu nie używam od dłuższego czasu – nie pamiętam jednak dokładnie jakie tutaj występowały aberracje, ale z jakiegoś powodu przesiadłem się na natywną funkcję .NET dla klasy string:
i bardzo długo tą funkcję uważałem za uniwersalny tester i myślałem że znalazłem moje panaceum… do czasu aż nie trafiłem na kolejne brzegowe sytuacje, które nieoczekiwanie zwracają głupotę. najpierw przykład oczywisty, tutaj nie ma co się dziwić:
kolejny nadal dość oczywisty przykład, choć warto zwrócić uwagę na olbrzymią różnię pomiędzy 'tablicą pustą’ z poprzedniego przykładu, a tablicą zawierającą element $null – zwróć uwagę na ilość elementów w tablicy:
i tu doszedłem do PS Borderlands – granic swojej wiedzy i możliwości zrozumienia mechanizmów pod spodem. dalszych przykładów nie potrafię wyjaśnić – znam je z obserwacji:
wychodzi na to że jedno nic to nadal nic, ale dwa nic to już coś. miałem na studiach matematykę nieskończoności, ale nigdy matematyki nicości, najwyraźniej muszę tą dziedzinę lepiej zgłębić (;
a dalej… to już kosmos. przypadek tak zawikłany, że zajęło mi masę czasu póki go wyizolowałem i umiałem pokazać na przykładzie. w przeszłości również trafiałem na podobne zachowanie, więc scenariuszy jest prawdopodobnie więcej, ale dopiero teraz postanowiłem to zrozumieć. opiszę, może znajdzie się harpagan, który to rozwikła…
poza granicami
jeśli funkcja implementuje 'ValueFromRemainingArguments’, typ zwracanego argumentu będzie zależny od tego czy się z tej funkcjonalności skorzysta. tak działa między innymi 'write-host’ – wywołanie polecenia:
bez cudzysłowiów – czyli de fakto polecenie powinno potraktować każdy wyraz jako oddzielą wartość kolejnego parametru. a jednak zinterpretował to jako pojedynczą wiadomość. to właśnie instrukcja ValueFromRemainingArguments upakowała wszystkie nienazwane wartości pod jedną zmienną. podobny myk stosuję w swoim write-log dzięki czemu mogę uzyskać podobny efekt. na tej też funkcji mogłem zbadać, że przy takim wywołaniu:
zmienna $message będzie typu [string]. ale przy takich wywołaniach:
zmienna $message jest typu [`list] [nie pytajcie skąd ten backtick w nazwie typu – bardzo dziwaczny to zapis, dodatkowa przyprawa w tym i tak dziwnym przypadq]. to pierwsza ważna różnica, ponieważ problem pojawia się właśnie przy tym typie. a że zazwyczaj ułatwiamy sobie życie i jeśli można ’-message’ nie pisać, to go nie piszemy, scenariusz więc dość codzienny.
jeśli jako wiadomość przekaże się obiekt, to zostanie stworzona lista zawierająca obiekty. można to zasymulować tak:
PS> $przekazaneWartosci = new-object System.Collections.Generic.List[system.object] PS> $mojObiekt = [PSCustomObject] @{page='w-files.pl';proto='https'} PS> $mojObiekt page proto ---- ----- w-files.pl https PS> $przekazaneWartosci.add($mojObiekt) PS> [string]::isNullOrEmpty($przekazaneWartosci) True
jeśli obiekt wrzucony do listy jest typu PSCustomObject – nawet jeśli nie jest pusty, funkcja zwraca informację, że lista jest pusta. można by powiedzieć – 'no ale panie, to jest metoda dla stringa, a tutaj masz obiekt, to pewnie dlatego’. tyle, że sprawdzałem na wielu innych typach obiektów – i zachowuje się prawidłowo.
póki co ten bug wydaje się działać tylko dla PSCustomObject(-> update dalej). już nawet taka zmiana w deklaracji, z custom na generic:spowoduje, że będzie działać prawidłowo. różnica pomiędzy PSObject a PSCustomObject mogłoby być też fajnym wpisem PS Borderlands i straciłem kiedyś sporo czasu na braq spostrzegawczości, że jest inna deklaracja… ale to nie teraz. dodatkowego smaczq dodaje fakt, że nie do końca wiadomo, kiedy coś zostanie przetworzone na PSCustomObject. ot proszę przykład:
PS> (get-process).gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array PS> (get-process|select processname).gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array PS> $ou='OU=borderlands,DC=w-files,DC=pl' PS> (Get-ADOrganizationalUnit -Identity $ou).gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True False ADOrganizationalUnit Microsoft.ActiveDirectory.Management.ADObject PS C:\CloudTeam-ScriptLAB> (Get-ADOrganizationalUnit -Identity $ou|select name).gettype() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True False PSCustomObject System.Object
z jakiegoś powodu filtrowanie niektórych typów obiektów zmienia ich typ – kolejna zagadka, już poza granicami zrozumienia, ponieważ to nie jest normalne zachowanie.
update
dopiero co skończyłem ten wpis i zabrałem się za kolejny skrypt. kolejny przykład obiektu, dla którego [string]::isNullOrEmpty zwraca głupotę – i to dużo prostszy. wiedziałem, że trafiałem na więcej takich przypadqw!
PS> $storageAccountList = get-AzStorageAccount -resourceGroupName 'myRG' PS> $storageAccountList StorageAccountName ResourceGroupName PrimaryLocation SkuName Kind AccessTier CreationTime Provisioning State ------------------ ----------------- --------------- ------- ---- ---------- ------------ ------------ nexortestsa myRG westeurope Standard_LRS StorageV2 Hot 07.05.2021 10:56:16 Succeeded PS> $storageAccountList.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True False PSStorageAccount System.Object PS> [string]::IsNullOrEmpty($storageAccountList) True
podsumowanie
cała informatyka opiera się na determiniźmie [no w każdym razie do czasu pojawienia się AI/ML, które jest w IT odpowiednikiem fizyki kwantowej w fizyce (; ]. brak determinizmu powoduje, że trzeba się nieźle napocić żeby wyłapać błędy i prawidłowo je obsłużyć.
strasznie mnie drażni, kiedy nie potrafię czegoś zrozumieć. takie 'niuanse’ to jakieś górne wartości zasady pareto – ten promil promila całego systemu, na który trzeba poświęcić mnóstwo, mnóstwo czasu, żeby w ogóle go uchwycić. nie wspominając o zrozumieniu. w tym przypadq – to jest kilka brzegowych sytuacji, które nakładają się na siebie. istna qmulacja. IMHO to nie jest coś 'do zrozumienia’ a bardziej błędy w implementacji, ale póki co – wpada do teczki w-Files.
eN.