使用安全殼層將應用程式連線至執行個體

如果您在執行個體上透過服務帳戶執行自動化工作,並與其他 Google Cloud Platform API 互動,則該服務帳戶可能也需要透過安全殼層存取其他 Compute Engine 執行個體。本教學課程說明如何將應用程式設為透過安全殼層連線存取您的執行個體。本教學課程的範例應用程式使用服務帳戶和 OS 登入進行安全殼層金鑰管理。

如要略過練習及檢視完整的程式碼範例,請造訪 GoogleCloudPlatform/python-docs-samples GitHub 頁面。

目標

本教學課程會說明如何達成以下目標:

  • 建立並設定服務帳戶,使其針對連線至執行個體的應用程式提供 OS 登入的安全殼層存取權。
  • 建立與您的服務帳戶相關聯的執行個體。
  • 在執行個體上設定範例應用程式,使其使用服務帳戶來管理自己的安全殼層金鑰與建立安全殼層連線。
  • 在與服務帳戶相關聯的執行個體上執行應用程式。
  • 在必須手動提供服務帳戶金鑰並指定其他安全殼層參數的 Compute Engine 外部位置執行應用程式。

費用

本教學課程使用 Compute Engine 等 GCP 計費元件。

初次使用 GCP 的使用者可能符合申請免費試用的資格。

事前準備

  1. 登入您的 Google 帳戶。

    如果您沒有帳戶,請申請新帳戶

  2. 在 GCP Console 的專案選擇器頁面中,選取或建立 GCP 專案。

    前往專案選取器頁面

  3. 請確認您已啟用 Google Cloud Platform 專案的計費功能。瞭解如何確認您已啟用專案的計費功能

  4. 您的使用者帳戶必須擁有建立、刪除及修改數個 Compute Engine 資源的權限。本教學課程假設您已經針對專案具有以下的身分與存取權管理角色
    • compute.instanceAdmin.v1
    • compute.networkAdmin
    • compute.osAdminLogin
    • iam.serviceAccountAdmin
    • iam.serviceAccountKeyAdmin
    • iam.serviceAccountUser
  5. 本教學課程假設您使用 Cloud Shell 執行gcloud 指令列工具指令。

建立及設定服務帳戶和範例執行個體

本教學課程使用單一服務帳戶與兩個執行個體來說明應用程式如何在遠端執行個體上執行安全殼層指令。

使用以下指令設定測試環境:

  1. 在 Console 中開啟 Cloud Shell:

    開啟 Cloud Shell

  2. 匯出環境變數,為後續指令設定專案 ID:

    export PROJECT_ID='[PROJECT_ID]'
    
  3. 在您的專案中建立新的服務帳戶。在這個範例中,請建立名為 ssh-account 的服務帳戶:

    gcloud iam service-accounts create ssh-account --project $PROJECT_ID \
       --display-name "ssh-account"
    
  4. 建立專供這個範例使用的臨時網路,並將其命名為 ssh-example

    gcloud compute networks create ssh-example --project $PROJECT_ID
    
  5. 建立防火牆規則,以允許所有連至 ssh-example 網路上執行個體的安全殼層連線:

    gcloud compute firewall-rules create ssh-all --project $PROJECT_ID \
       --network ssh-example --allow tcp:22
    
  6. us-central1-f 中建立名為 target 的執行個體。這個執行個體將會充當服務透過安全殼層連線的遠端執行個體。此執行個體必須已在專案層級或執行個體層級啟用 OS 登入功能。本範例說明如何使用 --metadata 旗標啟用這個特定執行個體的 OS 登入功能。請包含 --no-service-account--no-scopes 旗標,因為在此執行個體在這個特定的範例中不需要執行任何 API 要求:

    gcloud compute instances create target --project $PROJECT_ID \
       --zone us-central1-f --network ssh-example \
       --no-service-account --no-scopes \
       --machine-type f1-micro --metadata=enable-oslogin=TRUE
    
  7. compute.osAdminLogin 身分與存取權管理角色授予服務帳戶,使服務帳戶能夠和名為 target 的執行個體建立專屬的安全殼層連線。compute.osAdminLogin 角色也會將執行個體的超級使用者權限授予您的服務帳戶。雖然您可以在專案層級授予這個角色,讓角色套用到專案中的所有執行個體,但在本範例中,請特別在執行個體層級授予該角色,以便限制權限:

    gcloud compute instances add-iam-policy-binding target \
       --project $PROJECT_ID --zone us-central1-f \
       --member serviceAccount:ssh-account@$PROJECT_ID.iam.gserviceaccount.com \
       --role roles/compute.osAdminLogin
    
  8. us-central1-f 中建立名為 source 的執行個體,並將執行個體與 ssh-account 服務帳戶建立關聯。此外,也請指定 cloud-platform 範圍。服務帳戶需有這個範圍才能在這個執行個體上執行 API 要求:

    gcloud compute instances create source \
       --project $PROJECT_ID --zone us-central1-f \
       --service-account ssh-account@$PROJECT_ID.iam.gserviceaccount.com  \
       --scopes https://www.googleapis.com/auth/cloud-platform \
       --network ssh-example --machine-type f1-micro
    

服務帳戶現在可以管理自己的安全殼層金鑰組,並可使用安全殼層特別連線至 target 執行個體。由於 source 執行個體與您建立的 ssh-account 服務帳戶相關聯,因此 Python 適用的 Cloud 用戶端程式庫能夠以服務帳戶的身分使用應用程式預設憑證進行驗證,並使用您先前授予該服務帳戶的角色。

接著設定並執行可從某一執行個體透過安全殼層連線至另一個執行個體的應用程式。

在執行個體上執行安全殼層應用程式

如果在執行個體上執行的應用程式需要透過安全殼層存取其他執行個體,您可以透過程式管理服務帳戶的安全殼層金鑰組,並執行安全殼層指令。在本範例中,請透過下列程序來執行範例應用程式:

  1. 使用 gcloud 指令列工具連線至 source 執行個體:

    gcloud compute ssh source --project $PROJECT_ID --zone us-central1-f
    
  2. source 執行個體上,安裝 pip 和 Python 用戶端程式庫:

    my-username@source:~$ sudo apt update && sudo apt install python-pip -y && pip install --upgrade google-api-python-client
    
  3. GoogleCloudPlatform/python-docs-samples 下載 service_account_ssh.py 範例應用程式:

    my-username@source:~$ curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/python-docs-samples/master/compute/oslogin/service_account_ssh.py
    
  4. 執行範例應用程式,該應用程式使用 argparse 接受指令列的變數。在本範例中,請指示應用程式在 target 執行個體上安裝並執行 cowsay。在這個指令中,您要手動加入專案 ID:

    my-username@source:~$ python service_account_ssh.py \
        --cmd 'sudo apt install cowsay -y && cowsay "It works!"' \
        --project [PROJECT_ID] --zone us-central1-f --instance target
    
    ⋮
    ___________
      It works!
    -----------
           \   ^__^
            \  (oo)\_______
               (__)\       )\/\
                   ||----w |
                   ||     ||
    
    

如果應用程式正確執行,您將會收到來自 cowsay 應用程式的輸出結果。您可以修改 --cmd 旗標來包含任何所需的指令。或者您也可以自己編寫的應用程式,使應用程式直接匯入並呼叫 service_account_ssh.py

執行 exit 即可中斷與 source 執行個體的連線並返回 Cloud Shell。

在 Compute Engine 之外執行安全殼層應用程式

在先前的範例中,您在 Compute Engine 執行個體上執行應用程式,而在該執行個體中,Python 適用的 Cloud 用戶端程式庫可透過應用程式預設憑證使用與 source 執行個體相關聯的服務帳戶。如果您在 Compute Engine 執行個體以外的位置執行這個應用程式,則除非您手動提供服務帳戶金鑰,否則用戶端程式庫將無法存取服務帳戶及其權限。

  1. 針對先前在本教學課程中建立的 target 執行個體,取得該執行個體的外部 IP 位址。您可以在 Console 的「Instances」(執行個體) 頁面上找到此位址,或透過 gcloud 指令列工具執行下列指令來取得:

    gcloud compute instances describe target \
       --project $PROJECT_ID --zone us-central1-f
    
  2. 為您在先前範例中使用的 ssh-account 服務帳戶建立服務帳戶金鑰,並將金鑰檔案下載至本機工作站。

  3. 將服務帳戶金鑰複製到您要執行此範例的系統。

  4. 在要執行此範例的系統上開啟終端機。

  5. 設定 GOOGLE_APPLICATION_CREDENTIALS 環境變數,使其指向服務帳戶金鑰 .json 檔案所在的路徑。如果金鑰位於 Downloads 資料夾中,您設定的環境變數可能與以下範例類似:

    $ export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/key.json"
    
  6. 在此系統上安裝必要項目:

    1. 安裝 Pythonpip。在 Debian 系統上,您可以使用 apt 完成這項步驟:

      $ sudo apt update && sudo apt install python python-pip -y
      
    2. 使用 pip 安裝 Python 適用的 Cloud 用戶端程式庫:

      $ pip install --upgrade google-api-python-client
      
  7. 下載範例應用程式:

    $ curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/python-docs-samples/master/compute/oslogin/service_account_ssh.py
    
  8. 執行範例應用程式。在 Compute Engine 以外的位置執行該應用程式時,中繼資料伺服器將無法使用,因此您必須手動指定服務帳戶電子郵件地址。您也必須指定之前取得的 target 執行個體外部 IP 位址。

    $ python service_account_ssh.py \
        --cmd 'sudo apt install cowsay -y && cowsay "It works!"' \
        --account ssh-account@[PROJECT_ID].iam.gserviceaccount.com \
        --project [PROJECT_ID] --hostname [TARGET_EXTERNAL_IP]
    
    ⋮
    ___________
      It works!
    -----------
           \   ^__^
            \  (oo)\_______
               (__)\       )\/\
                   ||----w |
                   ||     ||
    
    

如果應用程式正確執行,您就會收到來自 cowsay 應用程式的輸出結果。

範例應用程式的運作方式

service_account_ssh.py 範例應用程式是透過以下程序運作:

  1. 初始化 OS Login API 物件。
  2. 如果您並未手動提供服務帳戶電子郵件地址,則應用程式會讀取執行個體中繼資料,以識別與執行個體相關聯的服務帳戶。如果在 Compute Engine 以外的位置執行此應用程式,則必須手動提供服務帳戶地址。
  3. 在執行此範例的執行個體上,呼叫 create_ssh_key() 方法來為服務帳戶產生臨時的安全殼層金鑰,並為服務帳戶新增具有到期計時器的公開金鑰,方便您指定期限。
  4. 從 OS Login API 呼叫 getLoginProfile() 方法,以取得服務帳戶使用的 POSIX 使用者名稱。
  5. 呼叫 run_ssh() 方法,以便使用服務帳戶的身分執行遠端安全殼層指令。
  6. 輸出遠端安全殼層指令的回應。
  7. 移除臨時的安全殼層 (SSH) 金鑰組檔案。
  8. OS 登入會在公開金鑰檔案到期後自動移除公開金鑰檔案。
def main(cmd, project, instance=None, zone=None,
         oslogin=None, account=None, hostname=None):
    """Run a command on a remote system."""

    # Create the OS Login API object.
    oslogin = oslogin or googleapiclient.discovery.build('oslogin', 'v1')

    # Identify the service account ID if it is not already provided.
    account = account or requests.get(
        SERVICE_ACCOUNT_METADATA_URL, headers=HEADERS).text
    if not account.startswith('users/'):
        account = 'users/' + account

    # Create a new SSH key pair and associate it with the service account.
    private_key_file = create_ssh_key(oslogin, account)

    # Using the OS Login API, get the POSIX user name from the login profile
    # for the service account.
    profile = oslogin.users().getLoginProfile(name=account).execute()
    username = profile.get('posixAccounts')[0].get('username')

    # Create the hostname of the target instance using the instance name,
    # the zone where the instance is located, and the project that owns the
    # instance.
    hostname = hostname or '{instance}.{zone}.c.{project}.internal'.format(
        instance=instance, zone=zone, project=project)

    # Run a command on the remote instance over SSH.
    result = run_ssh(cmd, private_key_file, username, hostname)

    # Print the command line output from the remote instance.
    # Use .rstrip() rather than end='' for Python 2 compatability.
    for line in result:
        print(line.decode('utf-8').rstrip('\n\r'))

    # Shred the private key and delete the pair.
    execute(['shred', private_key_file])
    execute(['rm', private_key_file])
    execute(['rm', private_key_file + '.pub'])

if __name__ == '__main__':

    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        '--cmd', default='uname -a',
        help='The command to run on the remote instance.')
    parser.add_argument(
        '--project',
        help='Your Google Cloud project ID.')
    parser.add_argument(
        '--zone',
        help='The zone where the target instance is locted.')
    parser.add_argument(
        '--instance',
        help='The target instance for the ssh command.')
    parser.add_argument(
        '--account',
        help='The service account email.')
    parser.add_argument(
        '--hostname',
        help='The external IP address or hostname for the target instance.')
    args = parser.parse_args()

    main(args.cmd, args.project, instance=args.instance, zone=args.zone,
         account=args.account, hostname=args.hostname)

create_ssh_key() 方法會產生新的安全殼層金鑰組。接著該方法會從 OS Login API 呼叫 users().importSshPublicKey(),以將公開金鑰與服務帳戶建立關聯。users().importSshPublicKey() 方法也接受指出公開金鑰剩餘有效時間長度的到期值。

def create_ssh_key(oslogin, account, private_key_file=None, expire_time=300):
    """Generate an SSH key pair and apply it to the specified account."""
    private_key_file = private_key_file or '/tmp/key-' + str(uuid.uuid4())
    execute(['ssh-keygen', '-t', 'rsa', '-N', '', '-f', private_key_file])

    with open(private_key_file + '.pub', 'r') as original:
        public_key = original.read().strip()

    # Expiration time is in microseconds.
    expiration = int((time.time() + expire_time) * 1000000)

    body = {
        'key': public_key,
        'expirationTimeUsec': expiration,
    }
    oslogin.users().importSshPublicKey(parent=account, body=body).execute()
    return private_key_file

最佳做法是將您的服務帳戶設為自行定期產生新金鑰組。在本範例中,服務帳戶會為其建立的每個安全殼層連線產生新的金鑰組,不過您可以修改這個行為,使其按照更符合應用程式需求的排程執行。

users().importSshPublicKey() 的要求主體包含 expirationTimeUsec 值,該值會告知 OS 登入金鑰的到期時間。每個帳戶最多只能有 32 KB 的安全殼層金鑰資料,因此最好將公開安全殼層金鑰設為在服務帳戶完成作業後不久就會到期。

服務帳戶完成設定安全殼層金鑰後,即可執行遠端指令。在本範例中,應用程式使用 run_ssh() 方法在遠端執行個體上執行指令並傳回指令輸出結果。

def run_ssh(cmd, private_key_file, username, hostname):
    """Run a command on a remote system."""
    ssh_command = [
        'ssh', '-i', private_key_file, '-o', 'StrictHostKeyChecking=no',
        '{username}@{hostname}'.format(username=username, hostname=hostname),
        cmd,
    ]
    ssh = subprocess.Popen(
        ssh_command, shell=False, stdout=subprocess.PIPE,
        stderr=subprocess.PIPE)
    result = ssh.stdout.readlines()
    return result if result else ssh.stderr.readlines()

清除所用資源

如要避免系統向您的 Google Cloud Platform 帳戶收取您在本教學課程中所用資源的相關費用:

使用下列指令清除測試環境中的資源:

  1. 在 Console 中開啟 Cloud Shell:

    開啟 Cloud Shell

  2. 刪除名為 source 的執行個體:

    gcloud compute instances delete source \
       --project $PROJECT_ID --zone us-central1-f
    
  3. 刪除名為 target 的執行個體:

    gcloud compute instances delete target \
       --project $PROJECT_ID --zone us-central1-f
    
  4. 刪除 ssh-account 服務帳戶:

    gcloud iam service-accounts delete ssh-account --project $PROJECT_ID
    
  5. 刪除名為 ssh-example 的網路:

    gcloud compute networks delete ssh-example --project $PROJECT_ID
    

後續步驟